diff --git a/doc/manual_installation/dovecot.rst b/doc/manual_installation/dovecot.rst index 99533f693..4d7cd6754 100644 --- a/doc/manual_installation/dovecot.rst +++ b/doc/manual_installation/dovecot.rst @@ -193,6 +193,49 @@ SQLite users Replace values between ``<>`` with yours. + +LDAP +==== +To make the LDAP authentication work, edit the :file:`conf.d/10-auth.conf` and +uncomment the following line at the end:: + + !include auth-ldap.conf.ext + +Then edit the :file:`conf.d/auth-ldap.conf.ext` and edit the passdb section as following. +You should comment the userdb section, which will be managed by SQL with modoboa database.:: + + passdb { + driver = ldap + + # Path for LDAP configuration file, see example-config/dovecot-ldap.conf.ext + args = /etc/dovecot/dovecot-ldap.conf.ext + } + + #userdb { + #driver = ldap + #args = /etc/dovecot/dovecot-ldap.conf.ext + + # Default fields can be used to specify defaults that LDAP may override + #default_fields = home=/home/virtual/%u + #} + +Your own dovecot LDAP configuration file is now :file:`/etc/dovecot/dovecot-ldap.conf.ext`. +You can add your default LDAP conf in it, following the `official documentation `_. + +Synchronize dovecot LDAP conf with modoboa LDAP conf +---------------------------------------------------- + +To make dovecot LDAP configuration synchronized with modoboa LDAP configuration, you should create a dedicated dovecot conf file. +At the end of your dovecot configuration file (:file:`dovecot-ldap.conf.ext`), add the following line:: + + !include_try dovecot-modoboa.conf.ext + +Then, set modoboa parameter *Enable Dovecot LDAP sync* to *Yes*. +Then set the *Dovecot LDAP config file* following the previous step (*/etc/dovecot/dovecot-modoboa.conf.ext* in the example) + +The last step is to add the command **update_dovecot_conf** to the cron job of modoboa. +Then, each time your modoboa LDAP configuration is updated, your dovecot LDAP configuration will also be. + LMTP ==== diff --git a/modoboa/admin/tests/test_need_dovecot_update.py b/modoboa/admin/tests/test_need_dovecot_update.py new file mode 100644 index 000000000..482eceafd --- /dev/null +++ b/modoboa/admin/tests/test_need_dovecot_update.py @@ -0,0 +1,84 @@ +"""Management command tests.""" + +import os +import shutil +import tempfile +from unittest import mock + +from django.core.management import call_command +from django.test import override_settings +from django.urls import reverse +from django.db import transaction + +from modoboa.core.tests.test_views import SETTINGS_SAMPLE + +from modoboa.lib.tests import ModoTestCase +from .. import factories, models + +from modoboa.core import models as core_models + +class NeedDovecotUpdateTestCase(ModoTestCase): + """Test need dovecot ldap update command.""" + + @classmethod + def setUpTestData(cls): + """Create test data.""" + super(NeedDovecotUpdateTestCase, cls).setUpTestData() + factories.populate_database() + + def setUp(self): + """Initiate initial env.""" + super(NeedDovecotUpdateTestCase, self).setUp() + self.workdir = tempfile.mkdtemp() + + def tearDown(self): + """Reset test env.""" + shutil.rmtree(self.workdir) + + def test_update_dovecot_update_state_valid_form(self): + self.assertFalse(self.localconfig.need_dovecot_update) + url = reverse("core:parameters") + settings = SETTINGS_SAMPLE.copy() + response = self.client.post(url, settings, format="json") + self.assertEqual(response.status_code, 200) + self.assertEqual(response.json(), "Parameters saved") + self.localconfig.refresh_from_db() + self.assertTrue(self.localconfig.need_dovecot_update) + + def test_update_dovecot_ldap_conf(self): + self.localconfig.need_dovecot_update = True + self.localconfig.save() + self.assertTrue(self.localconfig.need_dovecot_update) + + tmp_file = os.path.join(self.workdir, "test-dovecot-ldap.conf") + self.set_global_parameters({ + "authentication_type": "ldap", + "ldap_dovecot_sync": True, + "ldap_dovecot_conf_file": tmp_file, + "ldap_server_address": "localhost", + "ldap_server_port": "636", + "ldap_enable_secondary_server": True, + "ldap_secondary_server_address": "localhost2", + "ldap_secondary_server_port": "636", + "ldap_secured": "ssl", + "ldap_bind_dn": "DC=test,DC=lan", + "ldap_bind_password": "test", + "ldap_search_base": "CN=Users,DC=test,DC=lan", + "ldap_search_filter": "(& (objectClass=user) (|(mail=%(user)s)(sAMAccountName=%(user)s)) )" + }, app="core") + + # Generated file checks + call_command("update_dovecot_conf") + self.assertTrue(os.path.exists(tmp_file)) + + with open(tmp_file) as tmp: + content = tmp.read() + self.assertIn("uris = ldaps://localhost:636 ldaps://localhost2:636", content) + self.assertIn("dn = \"DC=test,DC=lan\"", content) + self.assertIn("dnpass = \'test\'", content) + self.assertIn("base = CN=Users,DC=test,DC=lan", content) + self.assertIn("user_filter = (& (objectClass=user) (|(mail=%u)(sAMAccountName=%u)) )", content) + self.assertIn("pass_filter = (& (objectClass=user) (|(mail=%u)(sAMAccountName=%u)) )", content) + + self.localconfig.refresh_from_db() + self.assertFalse(self.localconfig.need_dovecot_update) diff --git a/modoboa/core/app_settings.py b/modoboa/core/app_settings.py index cbda5556d..a18bd8a34 100644 --- a/modoboa/core/app_settings.py +++ b/modoboa/core/app_settings.py @@ -18,7 +18,6 @@ from modoboa.parameters import forms as param_forms, tools as param_tools from . import constants from . import sms_backends - def enabled_applications(): """Return the list of installed extensions.""" from modoboa.core.extensions import exts_pool @@ -369,6 +368,23 @@ class GeneralParametersForm(param_forms.AdminParametersForm): ), ) + ldap_dovecot_sync = YesNoField( + label=ugettext_lazy("Enable Dovecot LDAP sync"), + initial=False, + help_text=ugettext_lazy( + "Enable synchronisation of Dovecot LDAP parameters from Modoboa." + ) + ) + + ldap_dovecot_conf_file = forms.CharField( + label=ugettext_lazy("Dovecot LDAP config file"), + initial="/etc/dovecot/dovecot-modoboa.conf", + required=False, + help_text=ugettext_lazy( + "Dovecot LDAP configuration file location" + ) + ) + dash_sep = SeparatorField(label=ugettext_lazy("Dashboard")) rss_feed_url = forms.URLField( @@ -495,6 +511,7 @@ class GeneralParametersForm(param_forms.AdminParametersForm): "ldap_import_search_base": "ldap_enable_import=True", "ldap_import_search_filter": "ldap_enable_import=True", "ldap_import_username_attr": "ldap_enable_import=True", + "ldap_dovecot_conf_file": "ldap_dovecot_sync=True", "check_new_versions": "enable_api_communication=True", "send_statistics": "enable_api_communication=True", "send_new_versions_email": "check_new_versions=True", @@ -682,3 +699,7 @@ class GeneralParametersForm(param_forms.AdminParametersForm): from modoboa.lib.authbackends import LDAPSecondaryBackend self._apply_ldap_settings(values, LDAPSecondaryBackend) + + def to_dovecot_settings(self): + self.localconfig.need_dovecot_update = True + self.localconfig.save() diff --git a/modoboa/core/migrations/0021_localconfig_need_dovecot_update.py b/modoboa/core/migrations/0021_localconfig_need_dovecot_update.py new file mode 100644 index 000000000..152e07e71 --- /dev/null +++ b/modoboa/core/migrations/0021_localconfig_need_dovecot_update.py @@ -0,0 +1,18 @@ +# Generated by Django 2.2.10 on 2020-04-29 12:31 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0020_auto_20200421_0851'), + ] + + operations = [ + migrations.AddField( + model_name='localconfig', + name='need_dovecot_update', + field=models.BooleanField(default=False), + ), + ] diff --git a/modoboa/core/models.py b/modoboa/core/models.py index f0e54ccc0..0db1ccf24 100644 --- a/modoboa/core/models.py +++ b/modoboa/core/models.py @@ -446,6 +446,9 @@ class LocalConfig(models.Model): _parameters = jsonfield.JSONField(default={}) + # Dovecot LDAP update + need_dovecot_update = models.BooleanField(default=False) + def __init__(self, *args, **kwargs): """Load parameter manager.""" super(LocalConfig, self).__init__(*args, **kwargs) diff --git a/modoboa/core/tests/test_views.py b/modoboa/core/tests/test_views.py index ec95a62fe..c99186858 100644 --- a/modoboa/core/tests/test_views.py +++ b/modoboa/core/tests/test_views.py @@ -88,7 +88,8 @@ SETTINGS_SAMPLE = { "core-sms_password_recovery": False, "core-sms_provider": "", "core-sms_ovh_endpoint": "ovh-eu", - "core-password_recovery_msg": "" + "core-password_recovery_msg": "", + "core-ldap_dovecot_sync": False } diff --git a/modoboa/core/views/admin.py b/modoboa/core/views/admin.py index 881b396b9..e58edf3e5 100644 --- a/modoboa/core/views/admin.py +++ b/modoboa/core/views/admin.py @@ -35,6 +35,7 @@ def parameters(request, tplname="core/parameters.html"): if form.is_valid(): form.save() form.to_django_settings() + form.to_dovecot_settings() continue return render_to_json_response( {"form_errors": form.errors, "prefix": form.app}, status=400 diff --git a/modoboa/ldapsync/lib.py b/modoboa/ldapsync/lib.py index cb1526c41..288b62976 100644 --- a/modoboa/ldapsync/lib.py +++ b/modoboa/ldapsync/lib.py @@ -240,3 +240,41 @@ def import_accounts_from_ldap(config): user.save() # FIXME: handle delete and rename operations? + +def build_ldap_uri(config, node=""): + """ Building LDAP uris for dovecot conf """ + if node: + node += "_" + return "{}://{}:{}".format( + "ldaps" if config["ldap_secured"]=="ssl" else "ldap", + config["ldap_{}server_address".format(node)], + config["ldap_{}server_port".format(node)] + ) + +def update_dovecot_config_file(config): + """ Update dovecot configuration file from LDAP parameters """ + + conf_file = config["ldap_dovecot_conf_file"] + + # Hosts conf + uris = build_ldap_uri(config) + if config["ldap_enable_secondary_server"]: + uris += " " + build_ldap_uri(config, "secondary") + + # Auth conf + bind_dn = config["ldap_bind_dn"] + bind_pwd = config["ldap_bind_password"] + + # Search conf + base = config["ldap_search_base"] + user_filter = config["ldap_search_filter"].replace("(user)s","u") + + with open(conf_file, "w") as fp: + fp.write("""uris = {uris} +dn = "{bind_dn}" +dnpass = '{bind_pwd}' +base = {base} +user_filter = {user_filter} +pass_filter = {pass_filter} +""".format(uris=uris, bind_dn=bind_dn, bind_pwd=bind_pwd, base=base, user_filter=user_filter, pass_filter=user_filter) + ) diff --git a/modoboa/ldapsync/management/commands/update_dovecot_conf.py b/modoboa/ldapsync/management/commands/update_dovecot_conf.py new file mode 100644 index 000000000..35ddc2909 --- /dev/null +++ b/modoboa/ldapsync/management/commands/update_dovecot_conf.py @@ -0,0 +1,25 @@ +"""Command to populate local database from a remote LDAP directory.""" + +from django.core.management.base import BaseCommand +from django.conf import settings + +from modoboa.parameters import tools as param_tools + +from ... import lib +from modoboa.core import models + +class Command(BaseCommand): + """Command definition.""" + + help = "Update dovecot configuration file from modoboa LDAP parameters" + + def handle(self, *args, **options): + """Command entry point.""" + localconfig = models.LocalConfig.objects.first() + + if localconfig.need_dovecot_update: + config = dict(param_tools.get_global_parameters("core")) + if config["authentication_type"] == "ldap" and config["ldap_dovecot_sync"]: + lib.update_dovecot_config_file(config) + localconfig.need_dovecot_update = False + localconfig.save() diff --git a/modoboa/parameters/forms.py b/modoboa/parameters/forms.py index f35949b25..0c24a4fd6 100644 --- a/modoboa/parameters/forms.py +++ b/modoboa/parameters/forms.py @@ -87,6 +87,9 @@ class AdminParametersForm(GenericParametersForm): """Inject parameters into django settings module.""" pass + def to_dovecot_settings(self): + pass + class UserParametersForm(GenericParametersForm): """Base form to declare user level parameters."""