# pam.py - functions authentication, authorisation and session handling
#
# Copyright (C) 2010, 2011, 2012 Arthur de Jong
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 2.1 of the License, or (at your option) any later version.
#
# This library 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
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
# 02110-1301 USA

import logging
import socket

from ldap.filter import escape_filter_chars as escape
import ldap

import cfg
import common
import constants
import passwd


def try_bind(userdn, password):
    # open a new connection
    conn = ldap.initialize(cfg.uri)
    # bind using the specified credentials
    conn.simple_bind_s(userdn, password)
    # perform search for own object (just to do any kind of search)
    res = conn.search_s(userdn, ldap.SCOPE_BASE, '(objectClass=*)', ['dn', ])
    for entry in res:
        if entry[0] == userdn:
            return
    raise ldap.NO_SUCH_OBJECT()


class PAMRequest(common.Request):

    def validate_request(self, parameters):
        """This method checks the provided username for validity and fills
        in the DN if needed."""
        # check username for validity
        common.validate_name(parameters['username'])
        # look up user DN
        entry = passwd.uid2entry(self.conn, parameters['username'])
        if not entry:
            raise ValueError('%r: user not found' % parameters['username'])
        # save the DN
        parameters['userdn'] = entry[0]
        # get the "real" username
        value = passwd.attmap.get_rdn_value(entry[0], 'uid')
        if not value:
            # get the username from the uid attribute
            values = entry[1]['uid']
            if not values or not values[0]:
                logging.warning('%s: is missing a %s attribute', dn, passwd.attmap['uid'])
            value = values[0]
        # check the username
        if value and not common.isvalidname(value):
            raise ValueError('%s: has invalid %s attribute', dn, passwd.attmap['uid'])
        # check if the username is different and update it if needed
        if value != parameters['username']:
            logging.info('username changed from %r to %r', parameters['username'], value)
            parameters['username'] = value


class PAMAuthenticationRequest(PAMRequest):

    action = constants.NSLCD_ACTION_PAM_AUTHC

    def read_parameters(self, fp):
        return dict(username=fp.read_string(),
                    ignore_userdn=fp.read_string(),
                    service=fp.read_string(),
                    password=fp.read_string())
        #self.validate_request()
        # TODO: log call with parameters

    def write(self, parameters, code=constants.NSLCD_PAM_SUCCESS, msg=''):
        self.fp.write_int32(constants.NSLCD_RESULT_BEGIN)
        self.fp.write_string(parameters['username'])
        self.fp.write_string('')  # userdn
        self.fp.write_int32(code)  # authc
        self.fp.write_int32(constants.NSLCD_PAM_SUCCESS)  # authz
        self.fp.write_string(msg)  # authzmsg
        self.fp.write_int32(constants.NSLCD_RESULT_END)

    def handle_request(self, parameters):
        # if the username is blank and rootpwmoddn is configured, try to
        # authenticate as administrator, otherwise validate request as usual
        if not parameters['username'] and cfg.rootpwmoddn:
            # authenticate as rootpwmoddn
            userdn = cfg.rootpwmoddn
            # if the caller is root we will allow the use of rootpwmodpw
            if not parameters['password'] and self.calleruid == 0 and cfg.rootpwmodpw:
                password = cfg.rootpwmodpw
            elif parameters['password']:
                password = parameters['password']
            else:
                raise ValueError('password missing')
        else:
            self.validate_request(parameters)
            userdn = parameters['userdn']
            password = parameters['password']
        # try authentication
        try:
            try_bind(userdn, password)
            logging.debug('bind successful')
            self.write(parameters)
        except ldap.INVALID_CREDENTIALS, e:
            try:
                msg = e[0]['desc']
            except:
                msg = str(e)
            logging.debug('bind failed: %s', msg)
            self.write(parameters, constants.NSLCD_PAM_AUTH_ERR, msg)


class PAMAuthorisationRequest(PAMRequest):

    action = constants.NSLCD_ACTION_PAM_AUTHZ

    def read_parameters(self, fp):
        return dict(username=fp.read_string(),
                    ignore_userdn=fp.read_string(),
                    service=fp.read_string(),
                    ruser=fp.read_string(),
                    rhost=fp.read_string(),
                    tty=fp.read_string())
        # TODO: log call with parameters

    def write(self, parameters, code=constants.NSLCD_PAM_SUCCESS, msg=''):
        self.fp.write_int32(constants.NSLCD_RESULT_BEGIN)
        self.fp.write_string(parameters['username'])
        self.fp.write_string('')  # userdn
        self.fp.write_int32(code)
        self.fp.write_string(msg)
        self.fp.write_int32(constants.NSLCD_RESULT_END)

    def check_authzsearch(self, parameters):
        if not cfg.pam_authz_searches:
            return
        # escape all parameters
        variables = dict((k, escape(v)) for k, v in parameters.items())
        variables.update(
                hostname=escape(socket.gethostname()),
                fqdn=escape(socket.getfqdn()),
                dn=variables['userdn'],
                uid=variables['username'],
            )
        # go over all authz searches
        for x in cfg.pam_authz_searches:
            filter = x.value(variables)
            logging.debug('trying pam_authz_search "%s"', filter)
            search = common.Search(self.conn, filter=filter, attributes=('dn', ))
            try:
                dn, values = search.items().next()
            except StopIteration:
                logging.error('pam_authz_search "%s" found no matches', filter)
                raise
            logging.debug('pam_authz_search found "%s"', dn)

    def handle_request(self, parameters):
        # fill in any missing userdn, etc.
        self.validate_request(parameters)
        # check authorisation search
        try:
            self.check_authzsearch(parameters)
        except StopIteration:
            self.write(parameters, constants.NSLCD_PAM_PERM_DENIED,
                       'LDAP authorisation check failed')
            return
        # all tests passed, return OK response
        self.write(parameters)


#NSLCD_ACTION_PAM_SESS_O
#NSLCD_ACTION_PAM_SESS_C
#NSLCD_ACTION_PAM_PWMOD