Skip to content

Commit

Permalink
ldap: Add LDAP unittests in hostcfgd
Browse files Browse the repository at this point in the history
  • Loading branch information
davidpil2002 committed Dec 20, 2023
1 parent 34cac8c commit a3f9072
Show file tree
Hide file tree
Showing 17 changed files with 867 additions and 69 deletions.
9 changes: 9 additions & 0 deletions data/templates/common-auth-sonic.j2
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,15 @@ auth [success=2 default=ignore] pam_exec.so /usr/sbin/cache_radius
# Local
auth [success=done new_authtok_reqd=done default=ignore{{ ' auth_err=die maxtries=die' if not auth['failthrough'] }}] pam_unix.so nullok try_first_pass

{% elif auth['login'] == 'ldap,local' %}
auth [success=2 default=ignore] pam_ldap.so minimum_uid=1000 try_first_pass
auth [success=1 default=ignore] pam_unix.so nullok try_first_pass
{% elif auth['login'] == 'local,ldap' %}
auth [success=2 default=ignore] pam_unix.so nullok try_first_pass
auth [success=1 default=ignore] pam_ldap.so minimum_uid=1000 try_first_pass
{% elif auth['login'] == 'ldap' %}
auth [success=1 default=ignore] pam_ldap.so minimum_uid=1000 try_first_pass

{% else %}
auth [success=1 default=ignore] pam_unix.so nullok try_first_pass

Expand Down
17 changes: 17 additions & 0 deletions data/templates/ldap.conf.j2
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{{ ldap_cfg.cfg_servers(servers) }}

base {{ ldap_cfg.cfg_base(servers) }}

ldap_version {{ ldap_cfg.cfg_version(servers) }}

binddn {{ ldap_cfg.cfg_bind(servers) }}

bindpw {{ ldap_cfg.cfg_bindpw(servers) }}

port {{ ldap_cfg.cfg_port(servers) }}

scope {{ ldap_cfg.cfg_scope(servers) }}

timelimit {{ ldap_cfg.cfg_timeout(servers) }}

bind_timelimit {{ ldap_cfg.cfg_bind_timeout(servers) }}
41 changes: 41 additions & 0 deletions data/templates/nslcd.conf.j2
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# /etc/nslcd.conf
# nslcd configuration file. See nslcd.conf(5)
# for details.

# The user and group nslcd should run as.
uid nslcd
gid nslcd

# The location at which the LDAP server(s) should be reachable.
{{ ldap_cfg.cfg_servers(servers) }}

# The search base that will be used for all queries.
base {{ ldap_cfg.cfg_base(servers) }}


# The LDAP protocol version to use.
ldap_version {{ ldap_cfg.cfg_version(servers) }}

# The DN to bind with for normal lookups.
binddn {{ ldap_cfg.cfg_bind(servers) }}
bindpw {{ ldap_cfg.cfg_bindpw(servers) }}

# The DN used for password modifications by root.
#rootpwmoddn cn=admin,dc=example,dc=com

# SSL options
#ssl off
#tls_reqcert never
tls_cacertfile /etc/ssl/certs/ca-certificates.crt

# The search scope.
scope {{ ldap_cfg.cfg_scope(servers) }}

timelimit {{ ldap_cfg.cfg_timeout(servers) }}

bind_timelimit {{ ldap_cfg.cfg_bind_timeout(servers) }}

nss_initgroups_ignoreusers ALLLOCAL

nss_min_uid 1000

69 changes: 0 additions & 69 deletions scripts/ldap.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,18 +15,7 @@ class LdapCfg:
PORT = 389
SCOPE = "sub"
HOST = ""
HOSTNAME_CHECK = "no"
GROUP_BASE_DN = "ou=users,dc=example,dc=com"
GROUP_MEMBER_ATTR = "member"
IPV6 = 6
SSL_MODE = "none"
CERT_VERIFY = "try" # tls_reqcert never|allow|try|demand|hard
# Folder for CA certs
SSL_CACERT_FILE = "none"
SSL_CIPHERS = "all"
# CRL check is not implemented in current version of nslcd
SSL_CRL_CHECK = "none"
SSL_CRL_FILE = "default"

@staticmethod
def _do_cfg(_ldapsrvs_conf, attr, cfg_str):
Expand All @@ -53,10 +42,6 @@ def cfg_servers(_ldapsrvs_conf):
servers_resp = f"uri "
ldap_mode = "ldap"
port = LdapCfg._do_cfg(_ldapsrvs_conf, LdapCfg.PORT, 'port')
ssl_mode = LdapCfg._do_cfg(_ldapsrvs_conf, LdapCfg.PORT, 'ssl_mode')
if ssl_mode == 'ssl':
port = LdapCfg._do_cfg(_ldapsrvs_conf, LdapCfg.PORT, 'ssl_port')
ldap_mode = "ldaps"
for server in _ldapsrvs_conf:
ip = server.get('ip', LdapCfg.HOST)
try:
Expand Down Expand Up @@ -93,57 +78,3 @@ def cfg_timeout(_ldapsrvs_conf):
@staticmethod
def cfg_bind_timeout(_ldapsrvs_conf):
return LdapCfg._do_cfg(_ldapsrvs_conf, LdapCfg.TIMEOUT_BIND, 'bind_timeout')

@staticmethod
def cfg_ssl_mode(_ldapsrvs_conf):
ssl_mode = LdapCfg._do_cfg(_ldapsrvs_conf, LdapCfg.SSL_MODE, 'ssl_mode')
ssl_modes = {
"start-tls": "start_tls",
"ssl": "on",
"none": "off"
}
ret_ssl_mode = ssl_modes.get(ssl_mode, "")
return ret_ssl_mode

@staticmethod
def cfg_tls_reqcert(_ldapsrvs_conf):
ssl_mode = LdapCfg._do_cfg(_ldapsrvs_conf, LdapCfg.SSL_MODE, 'ssl_mode')
cert_verify_ret = ""
# cert verify is only active in case ssl or start-tls are active
if ssl_mode == 'ssl' or ssl_mode == 'start-tls':
cert_verify_db = LdapCfg._do_cfg(_ldapsrvs_conf, LdapCfg.CERT_VERIFY, 'cert_verify')
if cert_verify_db == "enabled":
cert_verify_ret = "tls_reqcert demand"
elif cert_verify_db == "disabled":
cert_verify_ret = "tls_reqcert never"
else:
syslog.syslog(syslog.LOG_WARNING, f"Cert verify contains an invalid value: {cert_verify_db}")
else:
cert_verify_ret = ""
return cert_verify_ret

@staticmethod
def cfg_ca_certfile(_ldapsrvs_conf):
default_ca_list = "/etc/ssl/certs/ca-certificates.crt"
ca_list = LdapCfg._do_cfg(_ldapsrvs_conf, LdapCfg.SSL_CACERT_FILE, 'ca_list')
# Use default ca -list file. And add certificate manaement API to get list of certtificates
if ca_list == 'none':
ca_list_ret = ""
else:
ca_list_ret = f"tls_cacertfile {default_ca_list}"
return ca_list_ret

@staticmethod
def cfg_tls_ciphers(_ldapsrvs_conf):
tls_ciphers_ret = ""
tls_ciphers = LdapCfg._do_cfg(_ldapsrvs_conf, LdapCfg.SSL_CIPHERS, 'tls_ciphers')
if tls_ciphers == "all":
tls_ciphers_ret = ""
elif tls_ciphers == "TLS1.2":
tls_ciphers_ret = f"tls_ciphers {TLS1_2}"
elif tls_ciphers == "TLS1.3":
tls_ciphers_ret = f"tls_ciphers {TLS1_3}"
else:
tls_ciphers_ret = ""
syslog.syslog(syslog.LOG_ERR, f"LDAP TLS cipher contains an invalid value:: {tls_ciphers}")
return tls_ciphers_ret
116 changes: 116 additions & 0 deletions tests/hostcfgd/hostcfgd_ldap_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import importlib.machinery
import importlib.util
import filecmp
import shutil
import os
import sys
from swsscommon import swsscommon

from parameterized import parameterized
from unittest import TestCase, mock
from tests.hostcfgd.test_ldap_vectors import HOSTCFGD_TEST_LDAP_VECTOR
from tests.common.mock_configdb import MockConfigDb, MockDBConnector
from sonic_py_common.general import getstatusoutput_noshell


test_path = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
modules_path = os.path.dirname(test_path)
scripts_path = os.path.join(modules_path, "scripts")
templates_path = os.path.join(modules_path, "data/templates")
output_path = os.path.join(test_path, "hostcfgd/output")
sample_output_path = os.path.join(test_path, "hostcfgd/sample_output")
sys.path.insert(0, modules_path)

# Load the file under test
hostcfgd_path = os.path.join(scripts_path, 'hostcfgd')
loader = importlib.machinery.SourceFileLoader('hostcfgd', hostcfgd_path)
spec = importlib.util.spec_from_loader(loader.name, loader)
hostcfgd = importlib.util.module_from_spec(spec)
loader.exec_module(hostcfgd)
sys.modules['hostcfgd'] = hostcfgd

# Mock swsscommon classes
hostcfgd.ConfigDBConnector = MockConfigDb
hostcfgd.DBConnector = MockDBConnector
hostcfgd.Table = mock.Mock()

class TestHostcfgdLDAP(TestCase):
"""
Test hostcfd daemon - LDAP
"""
def run_diff(self, file1, file2):
_, output = getstatusoutput_noshell(['diff', '-uR', file1, file2])
return output


@parameterized.expand(HOSTCFGD_TEST_LDAP_VECTOR)
def test_hostcfgd_ldap(self, test_name, test_data):
"""
Test LDAP hostcfd daemon initialization
Args:
test_name(str): test name
test_data(dict): test data which contains initial Config Db tables, and expected results
Returns:
None
"""

t_path = templates_path
op_path = output_path + "/" + test_name
sop_path = sample_output_path + "/" + test_name

hostcfgd.PAM_AUTH_CONF_TEMPLATE = t_path + "/common-auth-sonic.j2"
hostcfgd.NSS_TACPLUS_CONF_TEMPLATE = t_path + "/tacplus_nss.conf.j2"
hostcfgd.NSS_RADIUS_CONF_TEMPLATE = t_path + "/radius_nss.conf.j2"
hostcfgd.PAM_RADIUS_AUTH_CONF_TEMPLATE = t_path + "/pam_radius_auth.conf.j2"
hostcfgd.PAM_AUTH_CONF = op_path + "/common-auth-sonic"
hostcfgd.NSS_TACPLUS_CONF = op_path + "/tacplus_nss.conf"
hostcfgd.NSS_RADIUS_CONF = op_path + "/radius_nss.conf"
hostcfgd.NSS_CONF = op_path + "/nsswitch.conf"
hostcfgd.NSLCD_CONF = op_path + "/nslcd.conf"
hostcfgd.NSLCD_CONF_TEMPLATE = t_path + "/nslcd.conf.j2"
hostcfgd.ETC_PAMD_SSHD = op_path + "/sshd"
hostcfgd.ETC_PAMD_LOGIN = op_path + "/login"
hostcfgd.RADIUS_PAM_AUTH_CONF_DIR = op_path + "/"

shutil.rmtree( op_path, ignore_errors=True)
os.mkdir( op_path)

shutil.copyfile( sop_path + "/sshd.old", op_path + "/sshd")
shutil.copyfile( sop_path + "/login.old", op_path + "/login")

MockConfigDb.set_config_db(test_data["config_db"])
host_config_daemon = hostcfgd.HostConfigDaemon()

aaa = host_config_daemon.config_db.get_table('AAA')

try:
ldap_global = host_config_daemon.config_db.get_table('LDAP')
except:
ldap_global = []
try:
ldap_server = \
host_config_daemon.config_db.get_table('LDAP_SERVER')
except:
ldap_server = []

host_config_daemon.aaacfg.load(aaa,[],[],[] ,[] , ldap_global, ldap_server)

diff_output = ""
files_to_compare = ['common-auth-sonic', 'nslcd.conf']

# check output files exists
for name in files_to_compare:
if not os.path.isfile(sop_path + "/" + name):
raise ValueError('filename: %s not exit' % (sop_path + "/" + name))
if not os.path.isfile(op_path + "/" + name):
raise ValueError('filename: %s not exit' % (op_path + "/" + name))

# deep comparison
match, mismatch, errors = filecmp.cmpfiles(sop_path, op_path, files_to_compare, shallow=False)

if not match:
for name in files_to_compare:
diff_output += self.run_diff( sop_path + "/" + name,\
op_path + "/" + name).decode('utf-8')

self.assertTrue(len(diff_output) == 0, diff_output)
21 changes: 21 additions & 0 deletions tests/hostcfgd/sample_output/LDAP/common-auth-sonic
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
#THIS IS AN AUTO-GENERATED FILE
#
# /etc/pam.d/common-auth- authentication settings common to all services
# This file is included from other service-specific PAM config files,
# and should contain a list of the authentication modules that define
# the central authentication scheme for use on the system
# (e.g., /etc/shadow, LDAP, Kerberos, etc.). The default is to use the
# traditional Unix authentication mechanisms.
#
# here are the per-package modules (the "Primary" block)

auth [success=2 default=ignore] pam_ldap.so minimum_uid=1000 try_first_pass
auth [success=1 default=ignore] pam_unix.so nullok try_first_pass
#
# here's the fallback if no module succeeds
auth requisite pam_deny.so
# prime the stack with a positive return value if there isn't one already;
# this avoids us returning an error just because nothing sets a success code
# since the modules above will each just jump around
auth required pam_permit.so
# and here are more per-package modules (the "Additional" block)
21 changes: 21 additions & 0 deletions tests/hostcfgd/sample_output/LDAP/ldap.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
uri ldap://10.10.10.2/
uri ldap://10.10.10.1/


base ou=users,dc=example,dc=com

ldap_version 3

binddn

bindpw pass

port 389

scope sub

timelimit 3

bind_timelimit 5

pam_check_host_attr no
Loading

0 comments on commit a3f9072

Please sign in to comment.