mirror of
https://github.com/jwinterm/modoboa.git
synced 2026-10-17 05:41:25 +02:00
LDAP Authentication - Webmail / Dovecot (#1876)
* Adding settings options for dovecot ldap sync * Creating update_dovecot_conf command * Adding async command to update dovecot ldap conf * Adding unit tests * Adding port to dovecot generated uris & unit tests * Code improvement & doc update & tests update
This commit is contained in:
@@ -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 <https://doc.dovecot.org/configuration_manual/authentication/ldap/>`_.
|
||||
|
||||
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
|
||||
====
|
||||
|
||||
|
||||
@@ -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)
|
||||
@@ -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()
|
||||
|
||||
@@ -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),
|
||||
),
|
||||
]
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
)
|
||||
|
||||
@@ -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()
|
||||
@@ -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."""
|
||||
|
||||
Reference in New Issue
Block a user