# coding: utf-8

"""
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
<http://www.dirmgr.com/blog/2010/8/26/ldap-password-changes-in-active-directory.html>`_:

- 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.

"""

from __future__ import unicode_literals

import ldap

from django.conf import settings
from django.utils.translation import ugettext as _
from django.utils.encoding import force_bytes
from django.utils import six

from modoboa.core.password_hashers import get_password_hasher
from modoboa.lib.exceptions import InternalError
from modoboa.parameters import tools as param_tools


class LDAPAuthBackend(object):
    """LDAP authentication backend."""

    def __init__(self):
        param_tools.apply_to_django_settings()
        self.global_params = dict(param_tools.get_global_parameters("core"))
        self.server_uri = self._setting(
            "AUTH_LDAP_SERVER_URI", "ldap://localhost"
        )
        self.pwd_attr = self._setting("LDAP_PASSWORD_ATTR", "userPassword")
        self.ldap_ad = self._setting("LDAP_ACTIVE_DIRECTORY", False)
        self.conn = None
        self.user_filter = self._setting("LDAP_USER_FILTER", "")

    def _setting(self, name, default):
        try:
            value = getattr(settings, name)
        except AttributeError:
            value = default
        return value

    def _get_conn(self, dn, password):
        """Get a connection from the server."""
        kwargs = {}
        if six.PY3:
            kwargs['bytes_mode'] = False
        conn = ldap.initialize(self.server_uri, **kwargs)
        conn.set_option(ldap.OPT_X_TLS_DEMAND, True)
        conn.set_option(ldap.OPT_DEBUG_LEVEL, 255)
        conn.simple_bind_s(dn, password)
        return conn

    def connect_to_server(self, user, password):
        """Connect to the server according to configuration."""
        if self.conn is not None:
            return
        mode = self.global_params["ldap_auth_method"]
        if mode == "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)
        else:
            tpl = self._setting("AUTH_LDAP_USER_DN_TEMPLATE", "")
            self.user_dn = tpl % {"user": user}
            self.conn = self._get_conn(self.user_dn, password)

    def _find_user_dn(self, user):
        """Find the DN of the given user."""
        sbase = self.global_params["ldap_search_base"]
        sfilter = self.global_params["ldap_search_filter"]
        sfilter = sfilter % {"user": user}
        res = self.conn.search_s(sbase, ldap.SCOPE_SUBTREE, sfilter)
        try:
            dn = res[0][0]
        except IndexError:
            return None
        return dn

    def _crypt_password(self, clearpassword):
        """Overidding of the crypt_password function (LDAP compliant)

        :param clearpassword: the clear password
        :return: the encrypted password
        """
        scheme = self.global_params["password_scheme"]
        hasher = get_password_hasher(scheme.upper())("ldap")
        return hasher.encrypt(clearpassword)

    def update_user_password(self, user, password, newpassword):
        """Update user password."""
        self.connect_to_server(user, password)
        user_dn = self._find_user_dn(user)
        if self.ldap_ad:
            newpassword = (
                ('"%s"' % newpassword).encode('utf-16').lstrip('\377\376')
            )
        ldif = [(ldap.MOD_REPLACE,
                 self.pwd_attr,
                 force_bytes(self._crypt_password(newpassword)))]
        try:
            self.conn.modify_s(user_dn, ldif)
        except ldap.LDAPError as e:
            raise InternalError(
                _("Failed to update password: {}").format(e))
