MarcJHuber/event-driven-servers

mavis external python module

naciohr opened this issue · 10 comments

Dear Marc,

I'm having some trouble to make ldaps setup work. The same server configuration using plain ldap works correctly. The checks I've been doing point to a sort of negotiation problem with openssl as the ldaps server expects tls1.2 while mavis_tacplus_ldap.pl is somehow stuck starting on tls1.0.

As a consequence, when using ldaps the mavistest returns "No answer from LDAP backend". Using TLS option yields "TLS negotiation failed." but I believe it's not working in the server.

I tested using a python script and it's working, probably because it uses openldap - gnutls libraries instead of openssl. Therefore, I would like to know whether I can develop a python module to work with mavis. I read the Mavis document and it comments external modules "write the processed list (plus a result code) to stdout" but I don't know the exact format of that output or how to check it.

May you tell me what the expected output should be like, please?

Thanks and regards,

Hi,

the LDAP query Perl scripts don't come with a custom TLS implementation, so the default is whatever the underlying Perl module thinks it's best. Could you check whether

setenv TLS_OPTIONS = "sslversion => 'tlsv1_2'"

in module context makes a difference?

Thanks,

Marc

Hi,

I'm sorry, but I can't help you with that. That "openssl s_client" failure seems to imply a local issue.

Cheers,

Marc

Hi Marc,

The problem appears to be with openssl indeed openssl/openssl#19518. I tried to compile tac_plus-ng with openssl 1.1.1 but it requires 3.0. Is there a chance to use 1.1.1?

BR,

Hi,

TLS support for tac_plus-ng is experimental, and while standardization is in progress I'd expect it to take quite some time before actual network devices support it. tac_plus-ng can easily be built without TLS support (e.g. using "./configure --minimum tac_plus-ng").

Regarding a Python module implementation: Well, yes, that's certainly a possibility. I could commit a module for importing the constants (I've never bothered to release that -- I'm just not familiar enough with Python to support it).

The "protocol" between the "external" module and back-end scripts is pretty simple: script receives a series of

\n

lines, followed by a single

=\n

on stdin. It then does its task and returns the updated

\n

list, followed by a

=\n

to stdout. Typical values are 0 (final result available) and 16 (try next module). The perl back-ends should give some insights about the attribute-value pairs that may be used.

Just to make sure that your configuration is correct: Your LDAP_HOSTS variable looks like "ldaps://:636" and USE_TLS is not set? The latter would trigger START_TLS, and that's obviously not available for a native LDAPS server.

Cheers,

Marc

Hello,

Thank-you. I was able to make the python module work! Just managed the stdin and stdout as you commented. The perl script appears to translate AV constant strings into numbers before prompting, but using the numbers directly allows python to interact correctly.

I will clean the code a bit and will leave a sample here.

Regarding the configuration, that's correct. I use ldaps with port 636 and USE_TLS is not declared. That config on openssl 3.0 is not working; it does for openssl 1.1.1 and gnutls that python implements.

Best regards,

Hi,

ok, thanks. I'm curious when this will work again ...

I've just pushed some of the Python sample code I've played with a couple of weeks ago. Worked for me, but my Python background is quite limited.

Cheers,

Marc

Hello,

I had a look to the code you published. It was really helpful as I realised the tacacs may interact several times with the module rather than just pass all AVs at once.

I built this code that works for me against a microsoft ldaps. It's based on python-ldap. The logger can be used to log stuff to a file to help out the debugging when the tacacs is running.

This is subject to improvements such as object implementation, but it works for me anyway.

import ldap, os, sys, logging, re
from datetime import datetime

# Import AV constants
import mavis

logger = logging.getLogger(__name__)
logger.setLevel(logging.DEBUG)
formatLogger = logging.Formatter("{asctime} {message}", style = "{")
fileLogger = logging.FileHandler("ldappy.log", mode = "w", encoding='utf-8')
fileLogger.setFormatter(formatLogger)
logger.addHandler(fileLogger)

ldapHosts = os.environ.get('LDAP_HOSTS') or "ldaps://<server>"
ldapPass = os.environ.get('LDAP_PASSWD') or "<pass>"
ldapBase = os.environ.get('LDAP_BASE') or "<DN>"
ldapDomain = os.environ.get('LDAP_DOMAIN') or "<domain>"
ldapUser = "{}@{}".format(os.environ.get('LDAP_USER'), os.environ.get('LDAP_DOMAIN')) or "<user>"
ldapFilter = "(&(objectclass=user)(sAMAccountName={}))"

def parseStdin():
    avPairs = dict()
    for line in sys.stdin:
        #logger.info(line.rstrip())
        if line.rstrip() != "=":
            avPairs[int(line.split()[0])] = line.split()[1]
        else:
            return avPairs

def writeStdout(avPairs, result):
    for key in sorted(avPairs):
        sys.stdout.write("%d %s\n" % (int(key), avPairs[key]))
    sys.stdout.write("=%d\n" % result)

logger.info("start")
avDict = parseStdin()
result = mavis.MAVIS_DEFERRED
flagFinish = False
try:
    # ldapConnection = ldap.initialize(ldapHosts, trace_level=0)
    ldapConnection = ldap.ldapobject.ReconnectLDAPObject(ldapHosts, trace_level=0, retry_max=5, retry_delay=10.0)
    ldapConnection.set_option(ldap.OPT_DEFBASE, ldapBase)
    #ldapConnection.set_option(ldap.OPT_DEBUG_LEVEL,9)
except:
    avDict[mavis.AV_A_USER_RESPONSE] = "Unable to connect to the server."
    result = mavis.MAVIS_FINAL
    flagFinish = True

if not flagFinish and (avDict[mavis.AV_A_TYPE] != mavis.AV_V_TYPE_TACPLUS):
    result = mavis.MAVIS_DOWN
    flagFinish = True

if not flagFinish and (not mavis.AV_A_USER in avDict):
    avDict[mavis.AV_A_USER_RESPONSE] = "User not set."
    avDict[mavis.AV_A_RESULT] = mavis.AV_V_RESULT_ERROR
    result = mavis.MAVIS_FINAL
    flagFinish = True
else:
    ldapFilter = ldapFilter.format(avDict[mavis.AV_A_USER])

if not flagFinish and (re.match('\(|\)|,|\||&|\*', avDict[mavis.AV_A_USER])):
    avDict[mavis.AV_A_USER_RESPONSE] = "Username not valid."
    avDict[mavis.AV_A_RESULT] = mavis.AV_V_RESULT_ERROR;
    result = mavis.MAVIS_FINAL
    flagFinish = True

if not flagFinish and (avDict[mavis.AV_A_TACTYPE] == mavis.AV_V_TACTYPE_AUTH and not mavis.AV_A_PASSWORD in avDict):
    avDict[mavis.AV_A_USER_RESPONSE] = "Password not set."
    avDict[mavis.AV_A_RESULT] = mavis.AV_V_RESULT_ERROR;
    result = mavis.MAVIS_FINAL
    flagFinish = True

if not flagFinish and not mavis.AV_A_PASSWORD in avDict:
    ldapConnection.simple_bind_s(ldapUser, ldapPass)
    try:
        userData = ldapConnection.search_ext_s(ldapBase, ldap.SCOPE_SUBTREE, filterstr = ldapFilter)
        #logger.info("user data type {} with {}".format(type(userData), userData))
        if userData is not None and len(userData) != 0:
            avDict[mavis.AV_A_USER_RESPONSE] = "Authorization passed."
            avDict[mavis.AV_A_RESULT] = mavis.AV_V_RESULT_OK
            result = mavis.MAVIS_FINAL
        else:
            avDict[mavis.AV_A_USER_RESPONSE] = "User not found."
            avDict[mavis.AV_A_RESULT] = mavis.AV_V_RESULT_NOTFOUND
            result = mavis.MAVIS_DOWN
    except ldap.TIMEOUT as lto:
        #logger.info("timeout {}".format(lto))
        avDict[mavis.AV_A_USER_RESPONSE] = "Time out looking up the user."
        avDict[mavis.AV_A_RESULT] = mavis.AV_V_RESULT_ERROR
        result = mavis.MAVIS_TIMEOUT
    finally:
        ldapConnection.unbind_s()
        flagFinish = True

if not flagFinish and mavis.AV_A_PASSWORD in avDict:
    ldapConnection = ldap.ldapobject.ReconnectLDAPObject(ldapHosts, trace_level=0, retry_max=5, retry_delay=10.0)
    ldapConnection.set_option(ldap.OPT_DEFBASE, ldapBase)
    try:
        #logger.info("av received {}".format(avDict))
        #logger.info("mypass {}".format(avDict[mavis.AV_A_PASSWORD]))
        ldapConnection.simple_bind_s("{}@{}".format(avDict[mavis.AV_A_USER], ldapDomain), avDict[mavis.AV_A_PASSWORD])
        #logger.info("ok")
        userData = ldapConnection.search_ext_s(ldapBase, ldap.SCOPE_SUBTREE, filterstr = ldapFilter)
        #logger.info("user data type {} with {}".format(type(userData), userData))
        avDict[mavis.AV_A_DBPASSWORD] = avDict[mavis.AV_A_PASSWORD]
        avDict[mavis.AV_A_DN] = userData[0][0]
        avDict[mavis.AV_A_MEMBEROF] = '"{}"'.format(userData[0][1]['memberOf'][0].decode('UTF-8'))
        avDict[mavis.AV_A_RESULT] = mavis.AV_V_RESULT_OK
        result = mavis.MAVIS_FINAL
    except ldap.INVALID_CREDENTIALS as lic:
        #logger.info("invalid credentials {}".format(lic))
        avDict[mavis.AV_A_RESULT] = mavis.AV_V_RESULT_FAIL
        result = mavis.MAVIS_DOWN
    finally:
        flagFinish = True
        ldapConnection.unbind_s()

writeStdout(avDict, result)

Again, many thanks for your help and your time.

BR,

Hi,

pretty nice, thanks for sharing this code!

I'll be closing this issue then.

Cheers,

Marc