Blame pynslcd/pam.py

Packit 6bd9ab
Packit 6bd9ab
# pam.py - functions authentication, authorisation and session handling
Packit 6bd9ab
#
Packit 6bd9ab
# Copyright (C) 2010, 2011, 2012, 2013 Arthur de Jong
Packit 6bd9ab
#
Packit 6bd9ab
# This library is free software; you can redistribute it and/or
Packit 6bd9ab
# modify it under the terms of the GNU Lesser General Public
Packit 6bd9ab
# License as published by the Free Software Foundation; either
Packit 6bd9ab
# version 2.1 of the License, or (at your option) any later version.
Packit 6bd9ab
#
Packit 6bd9ab
# This library is distributed in the hope that it will be useful,
Packit 6bd9ab
# but WITHOUT ANY WARRANTY; without even the implied warranty of
Packit 6bd9ab
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
Packit 6bd9ab
# Lesser General Public License for more details.
Packit 6bd9ab
#
Packit 6bd9ab
# You should have received a copy of the GNU Lesser General Public
Packit 6bd9ab
# License along with this library; if not, write to the Free Software
Packit 6bd9ab
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
Packit 6bd9ab
# 02110-1301 USA
Packit 6bd9ab
Packit 6bd9ab
import logging
Packit 6bd9ab
import random
Packit 6bd9ab
import socket
Packit 6bd9ab
import time
Packit 6bd9ab
Packit 6bd9ab
from ldap.controls.ppolicy import PasswordPolicyControl, PasswordPolicyError
Packit 6bd9ab
from ldap.filter import escape_filter_chars
Packit 6bd9ab
import ldap
Packit 6bd9ab
Packit 6bd9ab
import cfg
Packit 6bd9ab
import common
Packit 6bd9ab
import constants
Packit 6bd9ab
import passwd
Packit 6bd9ab
import search
Packit 6bd9ab
import shadow
Packit 6bd9ab
Packit 6bd9ab
Packit 6bd9ab
random = random.SystemRandom()
Packit 6bd9ab
Packit 6bd9ab
Packit 6bd9ab
def authenticate(binddn, password):
Packit 6bd9ab
    # open a new connection
Packit 6bd9ab
    conn = search.Connection()
Packit 6bd9ab
    # bind using the specified credentials
Packit 6bd9ab
    pwctrl = PasswordPolicyControl()
Packit 6bd9ab
    res, data, msgid, ctrls = conn.simple_bind_s(binddn, password, serverctrls=[pwctrl])
Packit 6bd9ab
    # go over bind result server controls
Packit 6bd9ab
    for ctrl in ctrls:
Packit 6bd9ab
        if ctrl.controlType == PasswordPolicyControl.controlType:
Packit 6bd9ab
            # found a password policy control
Packit 6bd9ab
            logging.debug('PasswordPolicyControl found: error=%s (%s), timeBeforeExpiration=%s, graceAuthNsRemaining=%s',
Packit 6bd9ab
                'None' if ctrl.error is None else PasswordPolicyError(ctrl.error).prettyPrint(),
Packit 6bd9ab
                ctrl.error, ctrl.timeBeforeExpiration, ctrl.graceAuthNsRemaining)
Packit 6bd9ab
            if ctrl.error == 0:  # passwordExpired
Packit 6bd9ab
                return conn, constants.NSLCD_PAM_AUTHTOK_EXPIRED, PasswordPolicyError(ctrl.error).prettyPrint()
Packit 6bd9ab
            elif ctrl.error == 1:  # accountLocked
Packit 6bd9ab
                return conn, constants.NSLCD_PAM_ACCT_EXPIRED, PasswordPolicyError(ctrl.error).prettyPrint()
Packit 6bd9ab
            elif ctrl.error == 2:  # changeAfterReset
Packit 6bd9ab
                return conn, constants.NSLCD_PAM_NEW_AUTHTOK_REQD, 'Password change is needed after reset'
Packit 6bd9ab
            elif ctrl.error:
Packit 6bd9ab
                return conn, constants.NSLCD_PAM_PERM_DENIED, PasswordPolicyError(ctrl.error).prettyPrint()
Packit 6bd9ab
            elif ctrl.timeBeforeExpiration is not None:
Packit 6bd9ab
                return conn, constants.NSLCD_PAM_NEW_AUTHTOK_REQD, 'Password will expire in %d seconds' % ctrl.timeBeforeExpiration
Packit 6bd9ab
            elif ctrl.graceAuthNsRemaining is not None:
Packit 6bd9ab
                return conn, constants.NSLCD_PAM_NEW_AUTHTOK_REQD, 'Password expired, %d grace logins left' % ctrl.graceAuthNsRemaining
Packit 6bd9ab
    # perform search for own object (just to do any kind of search)
Packit 6bd9ab
    results = search.LDAPSearch(conn, base=binddn, scope=ldap.SCOPE_BASE,
Packit 6bd9ab
                                filter='(objectClass=*)', attributes=['dn', ])
Packit 6bd9ab
    for entry in results:
Packit 6bd9ab
        if entry[0] == binddn:
Packit 6bd9ab
            return conn, constants.NSLCD_PAM_SUCCESS, ''
Packit 6bd9ab
    # if our DN wasn't found raise an error to signal bind failure
Packit 6bd9ab
    raise ldap.NO_SUCH_OBJECT()
Packit 6bd9ab
Packit 6bd9ab
Packit 6bd9ab
def pwmod(conn, userdn, oldpassword, newpassword):
Packit 6bd9ab
    # perform request without old password
Packit 6bd9ab
    try:
Packit 6bd9ab
        conn.passwd_s(userdn, None, newpassword)
Packit 6bd9ab
    except ldap.LDAPError:
Packit 6bd9ab
        # retry with old password
Packit 6bd9ab
        if oldpassword:
Packit 6bd9ab
            conn.passwd_s(userdn, oldpassword, newpassword)
Packit 6bd9ab
        else:
Packit 6bd9ab
            raise
Packit 6bd9ab
Packit 6bd9ab
Packit 6bd9ab
def update_lastchange(conns, userdn):
Packit 6bd9ab
    """Try to update the shadowLastChange attribute of the entry."""
Packit 6bd9ab
    attribute = shadow.attmap['shadowLastChange']
Packit 6bd9ab
    if attribute == '${shadowLastChange:--1}':
Packit 6bd9ab
        attribute = 'shadowLastChange'
Packit 6bd9ab
    if not attribute or '$' in attribute:
Packit 6bd9ab
        raise ValueError('shadowLastChange has unsupported mapping')
Packit 6bd9ab
    # build the value for the new attribute
Packit 6bd9ab
    if attribute.lower() == 'pwdlastset':
Packit 6bd9ab
        # for AD we use another timestamp */
Packit 6bd9ab
        value = '%d000000000' % (time.time() / 100L + (134774L * 864L))
Packit 6bd9ab
    else:
Packit 6bd9ab
        # time in days since Jan 1, 1970
Packit 6bd9ab
        value = '%d' % (time.time() / (60 * 60 * 24))
Packit 6bd9ab
    # perform the modification, return at first success
Packit 6bd9ab
    for conn in conns:
Packit 6bd9ab
        try:
Packit 6bd9ab
            conn.modify_s(userdn, [(ldap.MOD_REPLACE, attribute, [value])])
Packit 6bd9ab
            return
Packit 6bd9ab
        except ldap.LDAPError:
Packit 6bd9ab
            pass  # ignore error and try next connection
Packit 6bd9ab
Packit 6bd9ab
Packit 6bd9ab
class PAMRequest(common.Request):
Packit 6bd9ab
Packit 6bd9ab
    def validate(self, parameters):
Packit 6bd9ab
        """This method checks the provided username for validity and fills
Packit 6bd9ab
        in the DN if needed."""
Packit 6bd9ab
        # check username for validity
Packit 6bd9ab
        common.validate_name(parameters['username'])
Packit 6bd9ab
        # look up user DN
Packit 6bd9ab
        entry = passwd.uid2entry(self.conn, parameters['username'])
Packit 6bd9ab
        if not entry:
Packit 6bd9ab
            # FIXME: we should close the stream with an empty response here
Packit 6bd9ab
            raise ValueError('%r: user not found' % parameters['username'])
Packit 6bd9ab
        # save the DN
Packit 6bd9ab
        parameters['userdn'] = entry[0]
Packit 6bd9ab
        # get the "real" username
Packit 6bd9ab
        value = passwd.attmap.get_rdn_value(entry[0], 'uid')
Packit 6bd9ab
        if not value:
Packit 6bd9ab
            # get the username from the uid attribute
Packit 6bd9ab
            values = entry[1]['uid']
Packit 6bd9ab
            if not values or not values[0]:
Packit 6bd9ab
                logging.warning('%s: is missing a %s attribute', entry[0], passwd.attmap['uid'])
Packit 6bd9ab
            value = values[0]
Packit 6bd9ab
        # check the username
Packit 6bd9ab
        if value and not common.is_valid_name(value):
Packit 6bd9ab
            raise ValueError('%s: has invalid %s attribute', entry[0], passwd.attmap['uid'])
Packit 6bd9ab
        # check if the username is different and update it if needed
Packit 6bd9ab
        if value != parameters['username']:
Packit 6bd9ab
            logging.info('username changed from %r to %r', parameters['username'], value)
Packit 6bd9ab
            parameters['username'] = value
Packit 6bd9ab
Packit 6bd9ab
Packit 6bd9ab
class PAMAuthenticationRequest(PAMRequest):
Packit 6bd9ab
Packit 6bd9ab
    action = constants.NSLCD_ACTION_PAM_AUTHC
Packit 6bd9ab
Packit 6bd9ab
    def read_parameters(self, fp):
Packit 6bd9ab
        return dict(username=fp.read_string(),
Packit 6bd9ab
                    service=fp.read_string(),
Packit 6bd9ab
                    ruser=fp.read_string(),
Packit 6bd9ab
                    rhost=fp.read_string(),
Packit 6bd9ab
                    tty=fp.read_string(),
Packit 6bd9ab
                    password=fp.read_string())
Packit 6bd9ab
        # TODO: log call with parameters
Packit 6bd9ab
Packit 6bd9ab
    def write(self, username, authc=constants.NSLCD_PAM_SUCCESS,
Packit 6bd9ab
              authz=constants.NSLCD_PAM_SUCCESS, msg=''):
Packit 6bd9ab
        self.fp.write_int32(constants.NSLCD_RESULT_BEGIN)
Packit 6bd9ab
        self.fp.write_int32(authc)
Packit 6bd9ab
        self.fp.write_string(username)
Packit 6bd9ab
        self.fp.write_int32(authz)
Packit 6bd9ab
        self.fp.write_string(msg)
Packit 6bd9ab
        self.fp.write_int32(constants.NSLCD_RESULT_END)
Packit 6bd9ab
Packit 6bd9ab
    def handle_request(self, parameters):
Packit 6bd9ab
        # if the username is blank and rootpwmoddn is configured, try to
Packit 6bd9ab
        # authenticate as administrator, otherwise validate request as usual
Packit 6bd9ab
        if not parameters['username'] and cfg.rootpwmoddn:
Packit 6bd9ab
            # authenticate as rootpwmoddn
Packit 6bd9ab
            binddn = cfg.rootpwmoddn
Packit 6bd9ab
            # if the caller is root we will allow the use of rootpwmodpw
Packit 6bd9ab
            if not parameters['password'] and self.calleruid == 0 and cfg.rootpwmodpw:
Packit 6bd9ab
                password = cfg.rootpwmodpw
Packit 6bd9ab
            elif parameters['password']:
Packit 6bd9ab
                password = parameters['password']
Packit 6bd9ab
            else:
Packit 6bd9ab
                raise ValueError('password missing')
Packit 6bd9ab
        else:
Packit 6bd9ab
            self.validate(parameters)
Packit 6bd9ab
            binddn = parameters['userdn']
Packit 6bd9ab
            password = parameters['password']
Packit 6bd9ab
        # try authentication
Packit 6bd9ab
        try:
Packit 6bd9ab
            conn, authz, msg = authenticate(binddn, password)
Packit 6bd9ab
        except ldap.INVALID_CREDENTIALS, e:
Packit 6bd9ab
            try:
Packit 6bd9ab
                msg = e[0]['desc']
Packit 6bd9ab
            except:
Packit 6bd9ab
                msg = str(e)
Packit 6bd9ab
            logging.debug('bind failed: %s', msg)
Packit 6bd9ab
            self.write(parameters['username'], authc=constants.NSLCD_PAM_AUTH_ERR, msg=msg)
Packit 6bd9ab
            return
Packit 6bd9ab
        if authz != constants.NSLCD_PAM_SUCCESS:
Packit 6bd9ab
            logging.warning('%s: %s: %s', binddn, parameters['username'], msg)
Packit 6bd9ab
        else:
Packit 6bd9ab
            logging.debug('bind successful')
Packit 6bd9ab
        # FIXME: perform shadow attribute checks with check_shadow()
Packit 6bd9ab
        self.write(parameters['username'], authz=authz, msg=msg)
Packit 6bd9ab
Packit 6bd9ab
Packit 6bd9ab
class PAMAuthorisationRequest(PAMRequest):
Packit 6bd9ab
Packit 6bd9ab
    action = constants.NSLCD_ACTION_PAM_AUTHZ
Packit 6bd9ab
Packit 6bd9ab
    def read_parameters(self, fp):
Packit 6bd9ab
        return dict(username=fp.read_string(),
Packit 6bd9ab
                    service=fp.read_string(),
Packit 6bd9ab
                    ruser=fp.read_string(),
Packit 6bd9ab
                    rhost=fp.read_string(),
Packit 6bd9ab
                    tty=fp.read_string())
Packit 6bd9ab
        # TODO: log call with parameters
Packit 6bd9ab
Packit 6bd9ab
    def write(self, authz=constants.NSLCD_PAM_SUCCESS, msg=''):
Packit 6bd9ab
        self.fp.write_int32(constants.NSLCD_RESULT_BEGIN)
Packit 6bd9ab
        self.fp.write_int32(authz)
Packit 6bd9ab
        self.fp.write_string(msg)
Packit 6bd9ab
        self.fp.write_int32(constants.NSLCD_RESULT_END)
Packit 6bd9ab
Packit 6bd9ab
    def check_authz_search(self, parameters):
Packit 6bd9ab
        if not cfg.pam_authz_searches:
Packit 6bd9ab
            return
Packit 6bd9ab
        # escape all parameters
Packit 6bd9ab
        variables = dict((k, escape_filter_chars(v)) for k, v in parameters.items())
Packit 6bd9ab
        variables.update(
Packit 6bd9ab
                hostname=escape_filter_chars(socket.gethostname()),
Packit 6bd9ab
                fqdn=escape_filter_chars(socket.getfqdn()),
Packit 6bd9ab
                dn=variables['userdn'],
Packit 6bd9ab
                uid=variables['username'],
Packit 6bd9ab
            )
Packit 6bd9ab
        # go over all authz searches
Packit 6bd9ab
        for x in cfg.pam_authz_searches:
Packit 6bd9ab
            filter = x.value(variables)
Packit 6bd9ab
            logging.debug('trying pam_authz_search "%s"', filter)
Packit 6bd9ab
            srch = search.LDAPSearch(self.conn, filter=filter, attributes=('dn', ))
Packit 6bd9ab
            try:
Packit 6bd9ab
                dn, values = srch.items().next()
Packit 6bd9ab
            except StopIteration:
Packit 6bd9ab
                logging.error('pam_authz_search "%s" found no matches', filter)
Packit 6bd9ab
                raise
Packit 6bd9ab
            logging.debug('pam_authz_search found "%s"', dn)
Packit 6bd9ab
Packit 6bd9ab
    def handle_request(self, parameters):
Packit 6bd9ab
        # fill in any missing userdn, etc.
Packit 6bd9ab
        self.validate(parameters)
Packit 6bd9ab
        # check authorisation search
Packit 6bd9ab
        try:
Packit 6bd9ab
            self.check_authz_search(parameters)
Packit 6bd9ab
        except StopIteration:
Packit 6bd9ab
            self.write(constants.NSLCD_PAM_PERM_DENIED,
Packit 6bd9ab
                       'LDAP authorisation check failed')
Packit 6bd9ab
            return
Packit 6bd9ab
        # all tests passed, return OK response
Packit 6bd9ab
        self.write()
Packit 6bd9ab
Packit 6bd9ab
Packit 6bd9ab
class PAMPasswordModificationRequest(PAMRequest):
Packit 6bd9ab
Packit 6bd9ab
    action = constants.NSLCD_ACTION_PAM_PWMOD
Packit 6bd9ab
Packit 6bd9ab
    def read_parameters(self, fp):
Packit 6bd9ab
        return dict(username=fp.read_string(),
Packit 6bd9ab
                    service=fp.read_string(),
Packit 6bd9ab
                    ruser=fp.read_string(),
Packit 6bd9ab
                    rhost=fp.read_string(),
Packit 6bd9ab
                    tty=fp.read_string(),
Packit 6bd9ab
                    asroot=fp.read_int32(),
Packit 6bd9ab
                    oldpassword=fp.read_string(),
Packit 6bd9ab
                    newpassword=fp.read_string())
Packit 6bd9ab
        # TODO: log call with parameters
Packit 6bd9ab
Packit 6bd9ab
    def write(self, rc=constants.NSLCD_PAM_SUCCESS, msg=''):
Packit 6bd9ab
        self.fp.write_int32(constants.NSLCD_RESULT_BEGIN)
Packit 6bd9ab
        self.fp.write_int32(rc)
Packit 6bd9ab
        self.fp.write_string(msg)
Packit 6bd9ab
        self.fp.write_int32(constants.NSLCD_RESULT_END)
Packit 6bd9ab
Packit 6bd9ab
    def handle_request(self, parameters):
Packit 6bd9ab
        # fill in any missing userdn, etc.
Packit 6bd9ab
        self.validate(parameters)
Packit 6bd9ab
        # check if pam_password_prohibit_message is set
Packit 6bd9ab
        if cfg.pam_password_prohibit_message:
Packit 6bd9ab
            self.write(constants.NSLCD_PAM_PERM_DENIED,
Packit 6bd9ab
                       cfg.pam_password_prohibit_message)
Packit 6bd9ab
            return
Packit 6bd9ab
        # check if the the user passed the rootpwmoddn
Packit 6bd9ab
        if parameters['asroot']:
Packit 6bd9ab
            binddn = cfg.rootpwmoddn
Packit 6bd9ab
            # check if rootpwmodpw should be used
Packit 6bd9ab
            if not parameters['oldpassword'] and self.calleruid == 0 and cfg.rootpwmodpw:
Packit 6bd9ab
                password = cfg.rootpwmodpw
Packit 6bd9ab
            elif parameters['oldpassword']:
Packit 6bd9ab
                password = parameters['oldpassword']
Packit 6bd9ab
            else:
Packit 6bd9ab
                raise ValueError('password missing')
Packit 6bd9ab
        else:
Packit 6bd9ab
            binddn = parameters['userdn']
Packit 6bd9ab
            password = parameters['oldpassword']
Packit 6bd9ab
            # TODO: check if shadow properties allow password change
Packit 6bd9ab
        # perform password modification
Packit 6bd9ab
        try:
Packit 6bd9ab
            conn, authz, msg = authenticate(binddn, password)
Packit 6bd9ab
            pwmod(conn, parameters['userdn'], parameters['oldpassword'], parameters['newpassword'])
Packit 6bd9ab
            # try to update lastchange with normal or user connection
Packit 6bd9ab
            update_lastchange((self.conn, conn), parameters['userdn'])
Packit 6bd9ab
        except ldap.INVALID_CREDENTIALS, e:
Packit 6bd9ab
            try:
Packit 6bd9ab
                msg = e[0]['desc']
Packit 6bd9ab
            except:
Packit 6bd9ab
                msg = str(e)
Packit 6bd9ab
            logging.debug('pwmod failed: %s', msg)
Packit 6bd9ab
            self.write(constants.NSLCD_PAM_PERM_DENIED, msg)
Packit 6bd9ab
            return
Packit 6bd9ab
        logging.debug('pwmod successful')
Packit 6bd9ab
        self.write()
Packit 6bd9ab
Packit 6bd9ab
Packit 6bd9ab
SESSION_ID_LENGTH = 25
Packit 6bd9ab
SESSION_ID_ALPHABET = (
Packit 6bd9ab
    "ABCDEFGHIJKLMNOPQRSTUVWXYZ" +
Packit 6bd9ab
    "abcdefghijklmnopqrstuvwxyz" +
Packit 6bd9ab
    "01234567890"
Packit 6bd9ab
)
Packit 6bd9ab
Packit 6bd9ab
Packit 6bd9ab
def generate_session_id():
Packit 6bd9ab
    return ''.join(
Packit 6bd9ab
        random.choice(SESSION_ID_ALPHABET)
Packit 6bd9ab
        for i in range(SESSION_ID_LENGTH)
Packit 6bd9ab
    )
Packit 6bd9ab
Packit 6bd9ab
Packit 6bd9ab
class PAMSessionOpenRequest(PAMRequest):
Packit 6bd9ab
Packit 6bd9ab
    action = constants.NSLCD_ACTION_PAM_SESS_O
Packit 6bd9ab
Packit 6bd9ab
    def read_parameters(self, fp):
Packit 6bd9ab
        return dict(username=fp.read_string(),
Packit 6bd9ab
                    service=fp.read_string(),
Packit 6bd9ab
                    ruser=fp.read_string(),
Packit 6bd9ab
                    rhost=fp.read_string(),
Packit 6bd9ab
                    tty=fp.read_string())
Packit 6bd9ab
        # TODO: log call with parameters
Packit 6bd9ab
Packit 6bd9ab
    def write(self, sessionid):
Packit 6bd9ab
        self.fp.write_int32(constants.NSLCD_RESULT_BEGIN)
Packit 6bd9ab
        self.fp.write_string(sessionid)
Packit 6bd9ab
        self.fp.write_int32(constants.NSLCD_RESULT_END)
Packit 6bd9ab
Packit 6bd9ab
    def handle_request(self, parameters):
Packit 6bd9ab
        # generate a session id
Packit 6bd9ab
        session_id = generate_session_id()
Packit 6bd9ab
        self.write(session_id)
Packit 6bd9ab
Packit 6bd9ab
Packit 6bd9ab
class PAMSessionCloseRequest(PAMRequest):
Packit 6bd9ab
Packit 6bd9ab
    action = constants.NSLCD_ACTION_PAM_SESS_C
Packit 6bd9ab
Packit 6bd9ab
    def read_parameters(self, fp):
Packit 6bd9ab
        return dict(username=fp.read_string(),
Packit 6bd9ab
                    service=fp.read_string(),
Packit 6bd9ab
                    ruser=fp.read_string(),
Packit 6bd9ab
                    rhost=fp.read_string(),
Packit 6bd9ab
                    tty=fp.read_string(),
Packit 6bd9ab
                    session_id=fp.read_string())
Packit 6bd9ab
        # TODO: log call with parameters
Packit 6bd9ab
Packit 6bd9ab
    def write(self):
Packit 6bd9ab
        self.fp.write_int32(constants.NSLCD_RESULT_BEGIN)
Packit 6bd9ab
        self.fp.write_int32(constants.NSLCD_RESULT_END)
Packit 6bd9ab
Packit 6bd9ab
    def handle_request(self, parameters):
Packit 6bd9ab
        self.write()