LDAP improvements:

* Support for domain adminstrator groups mapping (fixes #449)
* Support for better hash algorithm (see #450)
This commit is contained in:
Antoine Nguyen
2013-11-26 15:04:05 +01:00
parent 96aacbd484
commit 1d577d96bd
8 changed files with 94 additions and 42 deletions
+29 -4
View File
@@ -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", {
+3 -3
View File
@@ -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)
+12 -3
View File
@@ -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'))
+27 -2
View File
@@ -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)
+3 -1
View File
@@ -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:
+1 -1
View File
@@ -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"]
+12 -4
View File
@@ -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):
+7 -24
View File
@@ -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: