From babea7e61def9efaaddd7cdcd9225da68fd7bdbb Mon Sep 17 00:00:00 2001 From: Antoine Nguyen Date: Wed, 28 Dec 2011 13:30:42 +0100 Subject: [PATCH] Added missing file. see #192 --- lib/ldaputils.py | 109 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 109 insertions(+) create mode 100644 lib/ldaputils.py diff --git a/lib/ldaputils.py b/lib/ldaputils.py new file mode 100644 index 000000000..3db6841ff --- /dev/null +++ b/lib/ldaputils.py @@ -0,0 +1,109 @@ +# coding: utf-8 + +""" +:mod:`ldaputils` --- a collection of LDAP based classes/functions +----------------------------------------------------------------- + +For a first version, the LDAP support offered by Modoboa only supports +one global server definition : the one the django-auth-ldap uses. + +Extra information about Active Directory +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Extracted from `this blog +`_: + +- Active Directory doesn't appear to support the password modify + extended operation, so you must change passwords using a normal LDAP + modify operation. + +- Active Directory stores passwords in the unicodePwd attribute, + rather than userPassword. + +- Active Directory will only accept password changes over secure + connections. I have only ever used SSL. It may be that you can also + use StartTLS, or perhaps SASL with confidentiality, but I'm not sure + about that. + +- The new password must be enclosed in quotation marks, and it must + use a UTF-16 little-endian encoding. + +- Active Directory may impose some strength requirements on the + password, although exactly what those requirements are may vary from + one instance to another. + +""" +import ldap +import re +import base64 +from django.conf import settings +from modoboa.auth.lib import crypt_password +from modoboa.lib import parameters + +class LDAPException(Exception): pass + +class LDAPAuthBackend(object): + def __init__(self): + self.server_uri = self._setting("AUTH_LDAP_SERVER_URI", "ldap://localhost") + self.user_base = self._setting("LDAP_USER_BASE", "") + self.user_filter = self._setting("LDAP_USER_FILTER", "") + self.bind_dn = self._setting("AUTH_LDAP_BIND_DN", "") + self.bind_pwd = self._setting("AUTH_LDAP_BIND_PASSWORD", "") + self.pwd_attr = self._setting("LDAP_PASSWORD_ATTR", "userPassword") + self.ldap_ad = self._setting("LDAP_ACTIVE_DIRECTORY", False) + + self.conn = self._get_conn(self.bind_dn, self.bind_pwd) + + def _setting(self, name, default): + try: + value = getattr(settings, name) + except AttributeError: + value = default + return value + + def _get_conn(self, bind_dn, bind_pwd): + conn = ldap.initialize(self.server_uri) + conn.set_option(ldap.OPT_X_TLS_DEMAND, True) + conn.set_option(ldap.OPT_DEBUG_LEVEL, 255) + conn.simple_bind_s(bind_dn, bind_pwd) + return conn + + def _expand_filter(self, rawvalue, **kwargs): + return rawvalue % kwargs + + def _find_user_dn(self, user): + filtr = self._expand_filter(self.user_filter, user=user) + res = self.conn.search_s(self.user_base, ldap.SCOPE_SUBTREE, filtr) + try: + dn = res[0][0] + except IndexError: + return None + return dn + + 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") + crypted = crypt_password(clearpassword, False) + return str("{%s}%s" % (scheme.upper(), base64.encodestring(crypted))) + + def update_user_password(self, user, password, newpassword): + try: + dn = self._find_user_dn(user) + conn = self._get_conn(dn, password) + if self.ldap_ad: + newpassword = ('"%s"' % newpassword).encode('utf-16').lstrip('\377\376') + ldif = [(ldap.MOD_REPLACE, + self.pwd_attr, + self._crypt_password(newpassword))] + conn.modify_s(dn, ldif) + except ldap.LDAPError, e: + raise LDAPException(str(e)) + +