diff --git a/modoboa/core/app_settings.py b/modoboa/core/app_settings.py index 0194ba035..66175df6a 100644 --- a/modoboa/core/app_settings.py +++ b/modoboa/core/app_settings.py @@ -118,9 +118,9 @@ class GeneralParametersForm(parameters.AdminParametersForm): ) ldap_search_base = forms.CharField( - label=ugettext_lazy("Search base"), + label=ugettext_lazy("Users search base"), initial="", - help_text=ugettext_lazy("The distinguished name of the search base"), + help_text=ugettext_lazy("The distinguished name of the search base used to find users"), required=False ) @@ -150,6 +150,25 @@ class GeneralParametersForm(parameters.AdminParametersForm): 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="", + help_text=ugettext_lazy( + "Members of those LDAP groups will be created as domain " + "administrators. Use a ';' characters to separate groups" + ), + required=False + ) + + ldap_groups_search_base = forms.CharField( + label=ugettext_lazy("Groups search base"), + initial="", + help_text=ugettext_lazy( + "The distinguished name of the search base used to find groups" + ), + required=False + ) + sep3 = SeparatorField(label=ugettext_lazy("Miscellaneous")) log_maximum_age = forms.IntegerField( @@ -185,6 +204,8 @@ class GeneralParametersForm(parameters.AdminParametersForm): "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_groups_search_base": "authentication_type=ldap", } def __init__(self, *args, **kwargs): @@ -243,7 +264,7 @@ class GeneralParametersForm(parameters.AdminParametersForm): """ try: import ldap - from django_auth_ldap.config import LDAPSearch + from django_auth_ldap.config import LDAPSearch, PosixGroupType ldap_available = True except ImportError: ldap_available = False @@ -260,6 +281,11 @@ class GeneralParametersForm(parameters.AdminParametersForm): ldap_uri = 'ldaps://' if values["ldap_secured"] == "yes" else "ldap://" ldap_uri += "%s:%s" % (values["ldap_server_address"], values["ldap_server_port"]) setattr(settings, "AUTH_LDAP_SERVER_URI", ldap_uri) + setattr(settings, "AUTH_LDAP_GROUP_TYPE", PosixGroupType()) + setattr(settings, "AUTH_LDAP_GROUP_SEARCH", LDAPSearch( + values["ldap_groups_search_base"], ldap.SCOPE_SUBTREE, + "(objectClass=posixGroup)" + )) if values["ldap_auth_method"] == "searchbind": setattr(settings, "AUTH_LDAP_BIND_DN", values["ldap_bind_dn"]) setattr(settings, "AUTH_LDAP_BIND_PASSWORD", values["ldap_bind_password"]) @@ -270,7 +296,6 @@ class GeneralParametersForm(parameters.AdminParametersForm): setattr(settings, "AUTH_LDAP_USER_SEARCH", search) else: setattr(settings, "AUTH_LDAP_USER_DN_TEMPLATE", values["ldap_user_dn_template"]) - if values["ldap_is_active_directory"] == "yes": if not hasattr(settings, "AUTH_LDAP_GLOBAL_OPTIONS"): setattr(settings, "AUTH_LDAP_GLOBAL_OPTIONS", { diff --git a/modoboa/core/models.py b/modoboa/core/models.py index 3b906618d..390752594 100644 --- a/modoboa/core/models.py +++ b/modoboa/core/models.py @@ -108,7 +108,7 @@ class User(PermissionsMixin): :param raw_value: the new password's value :param curvalue: the current password (for LDAP authentication) """ - if parameters.get_admin("AUTHENTICATION_TYPE") == "local": + if self.is_local: self.password = self._crypt_password(raw_value) else: if not ldap_available: @@ -363,7 +363,7 @@ class User(PermissionsMixin): reversion.register(User) -def populate_callback(user): +def populate_callback(user, group='SimpleUsers'): """Populate callback If the LDAP authentication backend is in use, this callback will @@ -376,7 +376,7 @@ def populate_callback(user): from modoboa.lib.permissions import grant_access_to_object sadmins = User.objects.filter(is_superuser=True) - user.set_role("SimpleUsers") + user.set_role(group) user.post_create(sadmins[0]) for su in sadmins[1:]: grant_access_to_object(su, user) diff --git a/modoboa/core/password_hashers/advanced.py b/modoboa/core/password_hashers/advanced.py index aa844c9ea..3bb632ad2 100644 --- a/modoboa/core/password_hashers/advanced.py +++ b/modoboa/core/password_hashers/advanced.py @@ -16,7 +16,10 @@ class MD5CRYPTHasher(PasswordHasher): """ @property def scheme(self): - return '{MD5-CRYPT}' + return '{MD5-CRYPT}' if self._target == 'local' else '{CRYPT}' + + def _b64encode(self, pwhash): + return pwhash def _encrypt(self, clearvalue, salt=None): return md5_crypt.encrypt(clearvalue) @@ -34,7 +37,10 @@ class SHA256CRYPTHasher(PasswordHasher): """ @property def scheme(self): - return '{SHA256-CRYPT}' + return '{SHA256-CRYPT}' if self._target == 'local' else '{CRYPT}' + + def _b64encode(self, pwhash): + return pwhash def _encrypt(self, clearvalue, salt=None): rounds = int(parameters.get_admin('ROUNDS_NUMBER')) @@ -53,7 +59,10 @@ class SHA512CRYPTHasher(PasswordHasher): """ @property def scheme(self): - return '{SHA512-CRYPT}' + return '{SHA512-CRYPT}' if self._target == 'local' else '{CRYPT}' + + def _b64encode(self, pwhash): + return pwhash def _encrypt(self, clearvalue, salt=None): rounds = int(parameters.get_admin('ROUNDS_NUMBER')) diff --git a/modoboa/core/password_hashers/base.py b/modoboa/core/password_hashers/base.py index eb7ddd800..f264fd2d2 100644 --- a/modoboa/core/password_hashers/base.py +++ b/modoboa/core/password_hashers/base.py @@ -15,6 +15,22 @@ class PasswordHasher(object): """ Base class of all hashers. """ + def __init__(self, target='local'): + self._target = target + + def _encrypt(self, clearvalue, salt=None): + raise NotImplementedError + + def _b64encode(self, pwhash): + """Encode :keyword:`pwhash` using base64 if needed. + + :param str pwhash: password hash + :return: base64 encoded hash or original hash + """ + if self._target == 'ldap': + return base64.b64encode(pwhash) + return pwhash + def encrypt(self, clearvalue): """Encrypt a password. @@ -28,7 +44,8 @@ class PasswordHasher(object): :rtype: str :return: encrypted password """ - return '%s%s' % (self.scheme, self._encrypt(clearvalue)) + pwhash = self._b64encode(self._encrypt(clearvalue)) + return '%s%s' % (self.scheme, pwhash) def verify(self, clearvalue, hashed_value): """Verify a password against a hashed value. @@ -97,4 +114,12 @@ class SHA256Hasher(PasswordHasher): return '{SHA256}' def _encrypt(self, clearvalue, salt=None): - return base64.b64encode(hashlib.sha256(clearvalue).digest()) + return hashlib.sha256(clearvalue).digest() + + def _b64encode(self, pwhash): + """Encode :keyword:`pwhash` using base64. + + :param str pwhash: password hash + :return: base64 encoded hash + """ + return base64.b64encode(pwhash) diff --git a/modoboa/extensions/admin/__init__.py b/modoboa/extensions/admin/__init__.py index 7cc61f157..38ca5504c 100644 --- a/modoboa/extensions/admin/__init__.py +++ b/modoboa/extensions/admin/__init__.py @@ -184,8 +184,10 @@ def account_auto_created(user): from modoboa.core.models import User from modoboa.lib.permissions import grant_access_to_object - sadmins = User.objects.filter(is_superuser=True) localpart, domname = split_mailbox(user.username) + if user.group != 'SimpleUsers' and domname is None: + return + sadmins = User.objects.filter(is_superuser=True) try: domain = Domain.objects.get(name=domname) except Domain.DoesNotExist: diff --git a/modoboa/extensions/admin/forms/account.py b/modoboa/extensions/admin/forms/account.py index 2290196de..187a96894 100644 --- a/modoboa/extensions/admin/forms/account.py +++ b/modoboa/extensions/admin/forms/account.py @@ -65,7 +65,7 @@ class AccountFormGeneral(forms.ModelForm): account = kwargs["instance"] self.fields["role"].initial = account.group if not account.is_local \ - and parameters.get_admin("LDAP_AUTH_METHOD") == "directbind": + and parameters.get_admin("LDAP_AUTH_METHOD", app="core") == "directbind": del self.fields["password1"] del self.fields["password2"] diff --git a/modoboa/lib/authbackends.py b/modoboa/lib/authbackends.py index 3e9663604..c08555a4e 100644 --- a/modoboa/lib/authbackends.py +++ b/modoboa/lib/authbackends.py @@ -36,15 +36,23 @@ try: the user. ldap_user.dn is the user's DN and ldap_user.attrs contains all of their LDAP attributes. """ - lpart, domain = split_mailbox(username) - if domain is None: - return None + group = 'SimpleUsers' + admin_groups = parameters \ + .get_admin('LDAP_ADMIN_GROUPS', app='core').split(';') + for grp in admin_groups: + if grp.strip() in ldap_user.group_names: + group = 'DomainAdmins' + break + if group == 'SimpleUsers': + lpart, domain = split_mailbox(username) + if domain is None: + return None user, created = User.objects.get_or_create( username__iexact=username, defaults={'username': username.lower(), 'is_local': False} ) if created: - populate_callback(user) + populate_callback(user, group) return user, created def get_user(self, user_id): diff --git a/modoboa/lib/ldaputils.py b/modoboa/lib/ldaputils.py index ea4723af1..b83e0971c 100644 --- a/modoboa/lib/ldaputils.py +++ b/modoboa/lib/ldaputils.py @@ -34,13 +34,9 @@ Extracted from `this blog """ import ldap -import base64 -import hashlib -import string -import crypt -from random import Random from django.conf import settings from django.utils.translation import ugettext as _ +from modoboa.core.password_hashers import get_password_hasher from modoboa.lib import parameters from modoboa.lib.exceptions import InternalError @@ -71,7 +67,7 @@ class LDAPAuthBackend(object): return conn def connect_to_server(self, user, password): - if parameters.get_admin("LDAP_AUTH_METHOD", app="admin") == "searchbind": + if parameters.get_admin("LDAP_AUTH_METHOD", app="core") == "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) @@ -82,8 +78,8 @@ class LDAPAuthBackend(object): self.conn = self._get_conn(self.user_dn, password) def _find_user_dn(self, user): - sbase = parameters.get_admin("LDAP_SEARCH_BASE", app="admin") - sfilter = parameters.get_admin("LDAP_SEARCH_FILTER", app="admin") + sbase = parameters.get_admin("LDAP_SEARCH_BASE", app="core") + sfilter = parameters.get_admin("LDAP_SEARCH_FILTER", app="core") sfilter = sfilter % {"user": user} res = self.conn.search_s(sbase, ldap.SCOPE_SUBTREE, sfilter) try: @@ -95,25 +91,12 @@ class LDAPAuthBackend(object): def _crypt_password(self, clearpassword): """Overidding of the crypt_password function (LDAP compliant) - The crypted password in base64 encoded and we prepend the used - algorithm to the returned value. (between {}) - :param clearpassword: the clear password :return: the encrypted password """ - scheme = parameters.get_admin("PASSWORD_SCHEME", app="admin") - if scheme == "crypt": - salt = ''.join(Random().sample(string.letters + string.digits, 2)) - result = crypt.crypt(clearpassword, salt) - elif scheme == "md5": - obj = hashlib.md5(clearpassword) - result = obj.digest() - elif scheme == "sha256": - obj = hashlib.sha256(clearpassword) - result = obj.digest() - else: - return str(clearpassword) - return str("{%s}%s" % (scheme.upper(), base64.b64encode(result))) + scheme = parameters.get_admin("PASSWORD_SCHEME", app="core") + hasher = get_password_hasher(scheme.upper())('ldap') + return hasher.encrypt(clearpassword) def update_user_password(self, user, password, newpassword): try: