##########################################################################
#
# pgAdmin 4 - PostgreSQL Tools
#
# Copyright (C) 2013 - 2020, The pgAdmin Development Team
# This software is released under the PostgreSQL Licence
#
##########################################################################

"""A blueprint module implementing the ldap authentication."""

import ssl
import config
from ldap3 import Connection, Server, Tls, ALL, ALL_ATTRIBUTES
from ldap3.core.exceptions import LDAPSocketOpenError, LDAPBindError,\
    LDAPInvalidScopeError, LDAPAttributeError, LDAPInvalidFilterError,\
    LDAPStartTLSError, LDAPSSLConfigurationError
from flask_babelex import gettext

from .internal import BaseAuthentication
from pgadmin.model import User, ServerGroup, db, Role
from flask import current_app
from pgadmin.tools.user_management import create_user

try:
    from urllib.parse import urlparse
except ImportError:
    from urlparse import urlparse


ERROR_SEARCHING_LDAP_DIRECTORY = "Error searching the LDAP directory: {}"


class LDAPAuthentication(BaseAuthentication):
    """Ldap Authentication Class"""

    def get_friendly_name(self):
        return gettext("ldap")

    def authenticate(self, form):
        self.username = form.data['email']
        self.password = form.data['password']

        status, msg = self.connect()

        if not status:
            return status, msg

        status, user_email = self.search_ldap_user()

        if not status:
            return status, user_email

        return self.__auto_create_user(user_email)

    def connect(self):
        """Setup the connection to the LDAP server and authenticate the user.
        """

        # Parse the server URI
        uri = getattr(config, 'LDAP_SERVER_URI', None)

        if uri:
            uri = urlparse(uri)

        # Create the TLS configuration object if required
        tls = None

        if type(uri) == str:
            return False, "LDAP configuration error: Set the proper LDAP URI."

        if uri.scheme == 'ldaps' or config.LDAP_USE_STARTTLS:

            ca_cert_file = getattr(config, 'LDAP_CA_CERT_FILE', None)
            cert_file = getattr(config, 'LDAP_CERT_FILE', None)
            key_file = getattr(config, 'LDAP_KEY_FILE', None)
            cert_validate = ssl.CERT_NONE

            if ca_cert_file and cert_file and key_file:
                cert_validate = ssl.CERT_REQUIRED

            try:
                tls = Tls(
                    local_private_key_file=key_file,
                    local_certificate_file=cert_file,
                    validate=cert_validate,
                    version=ssl.PROTOCOL_TLSv1_2,
                    ca_certs_file=ca_cert_file)
            except LDAPSSLConfigurationError as e:
                current_app.logger.exception(
                    "LDAP configuration error: {}\n".format(e))
                return False, "LDAP configuration error: {}\n".format(
                    e.args[0])

        try:
            # Create the server object
            server = Server(uri.hostname,
                            port=uri.port,
                            use_ssl=(uri.scheme == 'ldaps'),
                            get_info=ALL,
                            tls=tls,
                            connect_timeout=config.LDAP_CONNECTION_TIMEOUT)
        except ValueError as e:
            return False, "LDAP configuration error: {}.".format(e)

        # Create the connection
        try:
            user_dn = "{0}={1},{2}".format(config.LDAP_USERNAME_ATTRIBUTE,
                                           self.username,
                                           config.LDAP_BASE_DN
                                           )
            self.conn = Connection(server,
                                   user=user_dn,
                                   password=self.password,
                                   auto_bind=True
                                   )

        except LDAPSocketOpenError as e:
            current_app.logger.exception(
                "Error connecting to the LDAP server: {}\n".format(e))
            return False, "Error connecting to the LDAP server:" \
                          " {}\n".format(e.args[0])
        except LDAPBindError as e:
            current_app.logger.exception(
                "Error binding to the LDAP server.")
            return False, "Error binding to the LDAP server."
        except Exception as e:
            current_app.logger.exception(
                "Error connecting to the LDAP server: {}\n".format(e))
            return False, "Error connecting to the LDAP server:" \
                          " {}\n".format(e.args[0])

        # Enable TLS if STARTTLS is configured
        if not uri.scheme == 'ldaps' and config.LDAP_USE_STARTTLS:
            try:
                self.conn.start_tls()
            except LDAPStartTLSError as e:
                current_app.logger.exception(
                    "Error starting TLS: {}\n".format(e))
                return False, "Error starting TLS: {}\n".format(e.args[0])

        return True, None

    def __auto_create_user(self, user_email):
        """Add the ldap user to the internal SQLite database."""
        if config.LDAP_AUTO_CREATE_USER:
            user = User.query.filter_by(
                username=self.username).first()
            if user is None:
                return create_user({
                    'username': self.username,
                    'email': user_email,
                    'role': 2,
                    'active': True,
                    'auth_source': 'ldap'
                })

        return True, None

    def search_ldap_user(self):
        """Get a list of users from the LDAP server based on config
         search criteria."""
        try:
            search_base_dn = config.LDAP_SEARCH_BASE_DN
            if search_base_dn is None or search_base_dn == '':
                search_base_dn = config.LDAP_BASE_DN
            self.conn.search(search_base=search_base_dn,
                             search_filter=config.LDAP_SEARCH_FILTER,
                             search_scope=config.LDAP_SEARCH_SCOPE,
                             attributes=ALL_ATTRIBUTES
                             )

        except LDAPInvalidScopeError as e:
            current_app.logger.exception(
                ERROR_SEARCHING_LDAP_DIRECTORY.format(e.args[0])
            )
            return False, ERROR_SEARCHING_LDAP_DIRECTORY.format(e.args[0])
        except LDAPAttributeError as e:
            current_app.logger.exception(
                ERROR_SEARCHING_LDAP_DIRECTORY.format(e)
            )
            return False, ERROR_SEARCHING_LDAP_DIRECTORY.format(e.args[0])
        except LDAPInvalidFilterError as e:
            current_app.logger.exception(
                ERROR_SEARCHING_LDAP_DIRECTORY.format(e)
            )
            return False, ERROR_SEARCHING_LDAP_DIRECTORY.format(e.args[0])

        for entry in self.conn.entries:
            user_email = None
            if config.LDAP_USERNAME_ATTRIBUTE in entry and self.username == \
                    entry[config.LDAP_USERNAME_ATTRIBUTE].value:
                if 'mail' in entry:
                    user_email = entry['mail'].value
                return True, user_email
        return False, ERROR_SEARCHING_LDAP_DIRECTORY.format(
            "Could not find the specified user.")
