Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

LDAP synchronization process #1732

Merged
merged 15 commits into from
May 7, 2019
2 changes: 1 addition & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
56 changes: 54 additions & 2 deletions doc/configuration.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
********************
Expand Down
101 changes: 81 additions & 20 deletions modoboa/core/app_settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")),
Expand Down Expand Up @@ -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="",
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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",
Expand All @@ -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:
Expand Down
1 change: 1 addition & 0 deletions modoboa/core/commands/templates/settings.py.tpl
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ MODOBOA_APPS = (
'modoboa.limits',
'modoboa.parameters',
'modoboa.dnstools',
'modoboa.ldapsync',
# Modoboa extensions here.
{% for extension in extensions %} '{{ extension }}',
{% endfor %}
Expand Down
5 changes: 5 additions & 0 deletions modoboa/core/tests/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
}


Expand Down
1 change: 1 addition & 0 deletions modoboa/ldapsync/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
default_app_config = "modoboa.ldapsync.apps.LdapsyncConfig"
8 changes: 8 additions & 0 deletions modoboa/ldapsync/apps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
from django.apps import AppConfig


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

def ready(self):
from . import handlers # noqa
36 changes: 36 additions & 0 deletions modoboa/ldapsync/handlers.py
Original file line number Diff line number Diff line change
@@ -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)
Loading