# -*- coding: utf-8 -*-
#
# repoze.who.plugins.ldap, LDAP authentication for WSGI applications.
# Copyright (C) 2008 by Gustavo Narea <http://gustavonarea.net/>
#
# This file is part of repoze.who.plugins.ldap
# <http://code.gustavonarea.net/repoze.who.plugins.ldap/>
#
# repoze.who.plugins.ldap is freedomware: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by the
# Free Software Foundation, either version 3 of the License, or any later
# version.
#
# repoze.who.plugins.ldap is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of 
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
# Public License for more details.
#
# You should have received a copy of the GNU General Public License along with
# repoze.who.plugins.ldap. If not, see <http://www.gnu.org/licenses/>.

"""LDAP plugins for repoze.who.

@todo: Write the function that creates an instance of LDAPFormPlugin.
@todo: Find how to put the redundant code of L{LDAPFormPlugin} and
    L{LDAPRedirectingFormPlugin} at a single place.

"""

__all__ = ['LDAPFormPlugin', 'LDAPRedirectingFormPlugin',
           'LDAPAuthenticatorPlugin']

from zope.interface import implements
import ldap

from repoze.who.interfaces import IAuthenticator
from repoze.who.plugins.form import FormPlugin, RedirectingFormPlugin

#{ Identifiers

class _BaseLDAPFormPlugin(object):
    """Base class to make L{FormPlugin} work with the LDAP authenticator"""
    
    def _get_dn(self, environ, identity):
        """
        Return the DN based on the environment and the identity.
        
        It prepends the user id to the base DN given in the constructor:
        
        If the C{login} item of the identity is C{rms} and the base DN is
        C{ou=developers,dc=gnu,dc=org}, the resulting DN will be:
        C{uid=rms,ou=developers,dc=gnu,dc=org}.
        
        @attention: You may want to override this method if the DN generated by
            default doesn't meet your requirements.
        @param environ: The WSGI environment.
        @param identity: The identity dictionary, provided by L{identify}.
        @return: The Distinguished Name (DN)
        @rtype: c{unicode}
        
        """
        try:
            return u'uid=%s,%s' % (identity['login'], self.base_dn)
        except (KeyError, TypeError):
            raise ValueError, ('Could not find the DN from the identity and '
                               'environment')


class LDAPFormPlugin(FormPlugin, _BaseLDAPFormPlugin):
    """Makes L{FormPlugin} work with the LDAP authenticator.
    
    It does the same as L{FormPlugin}, except that its identifier plugin is
    made to work with L{LDAPAuthenticatorPlugin} too.
    
    To use it, simply replace C{FormPlugin} by C{LDAPFormPlugin}. It's safe
    to use it, even without LDAP authentication.
    
    If the default way to find the DN is not suitable for you, you may want to
    override L{_get_dn}.
    
    @attention: You can use this as the plain L{FormPlugin} plugin, so you
        should check the documentation for L{FormPlugin} for more information.
    
    """
    
    def __init__(self, base_dn, *args, **kwargs):
        """Make L{FormPlugin} work with the LDAP authenticator.
        
        The additional arguments are those that should be passed to the
        constructor of L{FormPlugin}.
        
        @param base_dn: The base for the I{Distinguished Name}. Something like
            C{ou=employees,dc=example,dc=org}, to which will be prepended the
            user id: C{uid=jsmith,ou=employees,dc=example,dc=org}.
        @type base_dn: C{unicode}
        
        """
        super(LDAPFormPlugin, self).__init__(*args, **kwargs)
        self.base_dn = base_dn
    
    def identify(self, environ):
        """
        Identify with L{FormPlugin} and find the Distinguished Name.
        
        It identifies with L{FormPlugin.identify} and includes the
        I{Distinguished Name} in the C{identity} dictionary.
        
        @see: L{_get_dn}
        @warning: The C{login} must not invalidate the Distinguished Name.
        @param environ: The WSGI environment.
        @return: The C{identity} dictionary, including the C{dn} item.
        @rtype: C{dict}
        
        """
        identity = super(LDAPFormPlugin, self).identify(environ)
        try:
            identity['dn'] = self._get_dn(environ, identity)
            return identity
        except ValueError:
            return identity


class LDAPRedirectingFormPlugin(RedirectingFormPlugin, _BaseLDAPFormPlugin):
    """Makes L{RedirectingFormPlugin} work with the LDAP authenticator.
    
    It does the same as L{RedirectingFormPlugin}, except that its identifier 
    plugin is made to work with L{LDAPAuthenticatorPlugin} too.
    
    To use it, simply replace C{RedirectingFormPlugin} by
    C{LDAPRedirectingFormPlugin}. It's safe to use it, even without LDAP
    authentication.
    
    If the default way to find the DN is not suitable for you, you may want to
    override L{_get_dn}.
    
    @attention: You can use this as the plain L{RedirectingFormPlugin} plugin,
    so you should check the documentation for L{RedirectingFormPlugin} for more
    information.
    
    """
    
    def __init__(self, base_dn, *args, **kwargs):
        """Make L{RedirectingFormPlugin} work with the LDAP authenticator.
        
        The additional arguments are those that should be passed to the
        constructor of L{RedirectingFormPlugin}.
        
        @param base_dn: The base for the I{Distinguished Name}. Something like
            C{ou=employees,dc=example,dc=org}, to which will be prepended the
            user id: C{uid=jsmith,ou=employees,dc=example,dc=org}.
        @type base_dn: C{unicode}
        
        """
        super(LDAPRedirectingFormPlugin, self).__init__(*args, **kwargs)
        self.base_dn = base_dn
    
    def identify(self, environ):
        """
        Identify with L{RedirectingFormPlugin} and find the Distinguished Name.
        
        It identifies with L{RedirectingFormPlugin.identify} and includes the
        I{Distinguished Name} in the C{identity} dictionary.
        
        @see: L{_get_dn}
        @warning: The C{login} must not invalidate the Distinguished Name.
        @param environ: The WSGI environment.
        @return: The C{identity} dictionary, including the C{dn} item.
        @rtype: C{dict}
        
        """
        identity = super(LDAPRedirectingFormPlugin, self).identify(environ)
        try:
            identity['dn'] = self._get_dn(environ, identity)
            return identity
        except ValueError:
            return identity


#{ Authenticators

class LDAPAuthenticatorPlugin(object):

    implements(IAuthenticator)

    def __init__(self, ldap_connection):
        """Create an LDAP authentication plugin.
        
        By passing an existing LDAPObject, you're free to use the LDAP
        authentication method you want, the way you want.
        
        @param ldap_connection: An initialized LDAP connection.
        @type ldap_connection: ldap.LDAPObject
        
        """
        self.ldap_connection = ldap_connection

    # IAuthenticatorPlugin
    def authenticate(self, environ, identity):
        """Authenticate a given user with the Distinguished Name (DN) and
        password defined in 'identity'.
        
        @attention: The uid is not returned because it may not be unique; the
            DN, on the contrary, is always unique.
        @return: The Distinguished Name (DN).
        @rtype: C{unicode} or C{None}
        
        """
        
        try:
            dn = identity['dn']
            password = identity['password']
        except (KeyError, TypeError):
            return None

        if not hasattr(self.ldap_connection, 'simple_bind_s'):
            environ['repoze.who.logger'].warn('Cannot bind with the provided '
                                              'LDAP connection object')
            return None
        
        try:
            self.ldap_connection.simple_bind_s(dn, password)
            # The credentials are valid!
            return dn
        except ldap.LDAPError:
            return None

    def __repr__(self):
        return '<%s %s>' % (self.__class__.__name__, id(self))


#}
