Skip to content
Permalink
Browse files

LDAP synchronization process (#1732)

* Initial commit for LDAP sync process.

* Fixed unit tests.

* Added doc and run ldapsync tests on travis.

* Removed server address.

* Force bytes when calling ldap methods.

* Fixed tests compat with python2.

* Do not force bytes here.

* Force bytes.

* Force bytes for attribute names too.

* Disable bytes mode in PY2.

* Handle account removals.

* Fixed unit tests.

* Limit sync to simple users.

* Fixed unit test.

* Fixed unit test.
  • Loading branch information...
tonioo committed May 7, 2019
1 parent 7610933 commit ab592c3d02002391450c19a0df9e6ea9a1b916a8
@@ -50,7 +50,7 @@ before_script:
script:
- python ./tests.py
- cd test_project
- coverage run --source ../modoboa manage.py test modoboa.core modoboa.lib modoboa.admin modoboa.limits modoboa.transport modoboa.relaydomains modoboa.dnstools
- coverage run --source ../modoboa manage.py test modoboa.core modoboa.lib modoboa.admin modoboa.limits modoboa.transport modoboa.relaydomains modoboa.dnstools modoboa.ldapsync

after_success:
- coverage xml
@@ -14,7 +14,7 @@ are two available levels:

* User level: per user customization. Available at *User > Settings >
Preferences*

Regardless level, parameters are displayed using tabs, each tab
corresponding to one application.

@@ -405,7 +405,7 @@ group. In this case, the username is not necessarily an email address.
Users will also be able to update their LDAP password directly from
Modoboa.

.. note::
.. note::

Modoboa doesn't provide any synchronization mechanism once a user
is registered into the database. Any modification done from the
@@ -438,6 +438,58 @@ SMTP server location can be customized using the following settings:
AUTH_SMTP_SERVER_PORT = 25
AUTH_SMTP_SECURED_MODE = None # 'ssl' or 'starttls' are accepted

********************
LDAP synchronization
********************

Modoboa can synchronize accounts with an LDAP directory (tested with
OpenLDAP) but this feature is not enabled by default. To activate it,
add ``modoboa.ldapsync`` to ``MODOBOA_APPS`` in the
:file:`settings.py` file::

MODOBOA_APPS = (
'modoboa',
'modoboa.core',
'modoboa.lib',
'modoboa.admin',
'modoboa.transport',
'modoboa.relaydomains',
'modoboa.limits',
'modoboa.parameters',
'modoboa.dnstools',
'modoboa.ldapsync',
)

and enable it from the admin panel.

.. warning::

Make sure to install additional :ref:`requirements <ldap_auth>`
otherwise it won't work.

The following parameters are available and must be filled::

+--------------------+---------------------------------+--------------------+
|Name |Description |Default value |
+====================+=================================+====================+
|Enable LDAP |Control LDAP synchronization |no |
|synchronization |state | |
| | | |
+--------------------+---------------------------------+--------------------+
|Bind DN |The DN of a user with write | |
| |permission to create/update | |
| |accounts | |
+--------------------+---------------------------------+--------------------+
|Bind password |The associated password | |
| | | |
| | | |
+--------------------+---------------------------------+--------------------+
|Account DN template |The template used to build | |
| |account DNs (must contain a | |
| |%(user)s placeholder | |
+--------------------+---------------------------------+--------------------+


********************
Database maintenance
********************
@@ -114,6 +114,17 @@ class GeneralParametersForm(param_forms.AdminParametersForm):
"Use an SSL/STARTTLS connection to access the LDAP server")
)

ldap_is_active_directory = YesNoField(
label=ugettext_lazy("Active Directory"),
initial=False,
help_text=ugettext_lazy(
"Tell if the LDAP server is an Active Directory one")
)

# LDAP authentication settings
ldap_auth_sep = SeparatorField(
label=ugettext_lazy("LDAP authentication settings"))

ldap_auth_method = forms.ChoiceField(
label=ugettext_lazy("Authentication method"),
choices=[("searchbind", ugettext_lazy("Search and bind")),
@@ -178,20 +189,6 @@ class GeneralParametersForm(param_forms.AdminParametersForm):
widget=forms.TextInput(attrs={"class": "form-control"})
)

ldap_password_attribute = forms.CharField(
label=ugettext_lazy("Password attribute"),
initial="userPassword",
help_text=ugettext_lazy("The attribute used to store user passwords"),
widget=forms.TextInput(attrs={"class": "form-control"})
)

ldap_is_active_directory = YesNoField(
label=ugettext_lazy("Active Directory"),
initial=False,
help_text=ugettext_lazy(
"Tell if the LDAP server is an Active Directory one")
)

ldap_admin_groups = forms.CharField(
label=ugettext_lazy("Administrator groups"),
initial="",
@@ -220,6 +217,67 @@ class GeneralParametersForm(param_forms.AdminParametersForm):
required=False
)

# LDAP sync. settings
ldap_sync_sep = SeparatorField(
label=ugettext_lazy("LDAP synchronization settings"))

ldap_enable_sync = YesNoField(
label=ugettext_lazy("Enable LDAP synchronization"),
initial=False,
help_text=ugettext_lazy(
"Enable automatic synchronization between local database and "
"LDAP directory")
)

ldap_sync_delete_remote_account = YesNoField(
label=ugettext_lazy(
"Delete remote LDAP account when local account is deleted"
),
initial=False,
help_text=ugettext_lazy(
"Delete remote LDAP account when local account is deleted, "
"otherwise it will be disabled."
)
)

ldap_sync_bind_dn = forms.CharField(
label=ugettext_lazy("Bind DN"),
initial="",
help_text=ugettext_lazy(
"The distinguished name to use when binding to the LDAP server. "
"Leave empty for an anonymous bind"
),
required=False,
)

ldap_sync_bind_password = forms.CharField(
label=ugettext_lazy("Bind password"),
initial="",
help_text=ugettext_lazy(
"The password to use when binding to the LDAP server "
"(with 'Bind DN')"
),
widget=forms.PasswordInput(render_value=True),
required=False
)

ldap_sync_account_dn_template = forms.CharField(
label=ugettext_lazy("Account DN template"),
initial="",
help_text=ugettext_lazy(
"The template used to construct an account's DN. It should contain "
"one placeholder (ie. %(user)s)"
),
required=False
)

ldap_password_attribute = forms.CharField(
label=ugettext_lazy("Password attribute"),
initial="userPassword",
help_text=ugettext_lazy("The attribute used to store user passwords"),
widget=forms.TextInput(attrs={"class": "form-control"})
)

dash_sep = SeparatorField(label=ugettext_lazy("Dashboard"))

rss_feed_url = forms.URLField(
@@ -319,18 +377,13 @@ class GeneralParametersForm(param_forms.AdminParametersForm):

# Visibility rules
visibility_rules = {
"ldap_sep": "authentication_type=ldap",
"ldap_server_address": "authentication_type=ldap",
"ldap_server_port": "authentication_type=ldap",
"ldap_secured": "authentication_type=ldap",
"ldap_auth_sep": "authentication_type=ldap",
"ldap_auth_method": "authentication_type=ldap",
"ldap_bind_dn": "ldap_auth_method=searchbind",
"ldap_bind_password": "ldap_auth_method=searchbind",
"ldap_search_base": "ldap_auth_method=searchbind",
"ldap_search_filter": "ldap_auth_method=searchbind",
"ldap_user_dn_template": "ldap_auth_method=directbind",
"ldap_password_attribute": "authentication_type=ldap",
"ldap_is_active_directory": "authentication_type=ldap",
"ldap_admin_groups": "authentication_type=ldap",
"ldap_group_type": "authentication_type=ldap",
"ldap_groups_search_base": "authentication_type=ldap",
@@ -350,6 +403,14 @@ def clean_ldap_user_dn_template(self):
raise forms.ValidationError(_("Invalid syntax"))
return tpl

def clean_ldap_sync_account_dn_template(self):
tpl = self.cleaned_data["ldap_sync_account_dn_template"]
try:
tpl % {"user": "toto"}
except (KeyError, ValueError):
raise forms.ValidationError(_("Invalid syntax"))
return tpl

def clean_rounds_number(self):
value = self.cleaned_data["rounds_number"]
if value < 1000 or value > 999999999:
@@ -83,6 +83,7 @@ MODOBOA_APPS = (
'modoboa.limits',
'modoboa.parameters',
'modoboa.dnstools',
'modoboa.ldapsync',
# Modoboa extensions here.
{% for extension in extensions %} '{{ extension }}',
{% endfor %}
@@ -74,6 +74,11 @@
"admin-enable_dkim_checks": True,
"admin-enable_dmarc_checks": True,
"admin-enable_autoconfig_checks": True,
"core-ldap_enable_sync": False,
"core-ldap_sync_bind_dn": "",
"core-ldap_sync_bind_password": "",
"core-ldap_sync_account_dn_template": "",
"core-ldap_sync_delete_remote_account": False
}


@@ -0,0 +1 @@
default_app_config = "modoboa.ldapsync.apps.LdapsyncConfig"
@@ -0,0 +1,8 @@
from django.apps import AppConfig


class LdapsyncConfig(AppConfig):
name = 'modoboa.ldapsync'

def ready(self):
from . import handlers # noqa
@@ -0,0 +1,36 @@
"""LDAP sync related handlers."""

from django.db.models import signals
from django.dispatch import receiver

from modoboa.core import models as core_models
from modoboa.parameters import tools as param_tools

from . import lib


@receiver(signals.post_save, sender=core_models.User)
def sync_ldap_account(sender, instance, created, **kwargs):
"""Create/modify a new LDAP account if needed."""
config = dict(param_tools.get_global_parameters("core"))
if not config["ldap_enable_sync"]:
return
if created:
return
if instance.role != "SimpleUsers":
return
update_fields = kwargs.get("update_fields")
if update_fields and "last_login" in update_fields:
return
lib.update_ldap_account(instance, config)


@receiver(signals.pre_delete, sender=core_models.User)
def delete_ldap_account(sender, instance, **kwargs):
"""Delete LDAP account if needed."""
config = dict(param_tools.get_global_parameters("core"))
if not config["ldap_enable_sync"]:
return
if instance.role != "SimpleUsers":
return
lib.delete_ldap_account(instance, config)
Oops, something went wrong.

0 comments on commit ab592c3

Please sign in to comment.
You can’t perform that action at this time.