diff options
-rw-r--r-- | Makefile.am | 28 | ||||
-rw-r--r-- | configure.ac | 6 | ||||
-rw-r--r-- | contrib/ci/configure.sh | 1 | ||||
-rw-r--r-- | contrib/ci/deps.sh | 11 | ||||
-rwxr-xr-x | contrib/ci/run | 9 | ||||
-rw-r--r-- | src/external/cwrap.m4 | 7 | ||||
-rw-r--r-- | src/external/intgcheck.m4 | 32 | ||||
-rw-r--r-- | src/external/ldap.m4 | 4 | ||||
-rw-r--r-- | src/tests/intg/Makefile.am | 61 | ||||
-rw-r--r-- | src/tests/intg/config.py.m4 | 13 | ||||
-rw-r--r-- | src/tests/intg/ds.py | 61 | ||||
-rw-r--r-- | src/tests/intg/ds_openldap.py | 279 | ||||
-rw-r--r-- | src/tests/intg/ent.py | 470 | ||||
-rw-r--r-- | src/tests/intg/ent_test.py | 417 | ||||
-rw-r--r-- | src/tests/intg/ldap_ent.py | 102 | ||||
-rw-r--r-- | src/tests/intg/ldap_test.py | 261 | ||||
-rw-r--r-- | src/tests/intg/util.py | 55 |
17 files changed, 1811 insertions, 6 deletions
diff --git a/Makefile.am b/Makefile.am index 322f49887..1970b812e 100644 --- a/Makefile.am +++ b/Makefile.am @@ -19,7 +19,7 @@ if HAVE_MANPAGES SUBDIRS += src/man endif -SUBDIRS += . src/tests/cwrap +SUBDIRS += . src/tests/cwrap src/tests/intg # Some old versions of automake don't define builddir builddir ?= . @@ -2424,6 +2424,32 @@ autofs_test_client_CFLAGS = $(AM_CFLAGS) autofs_test_client_LDADD = -lpopt $(CLIENT_LIBS) endif +##################### +# Integration tests # +##################### + +intgcheck: + set -e; \ + rm -Rf intg; \ + $(MKDIR_P) intg/bld; \ + : Use /hopefully/ short prefix to keep D-Bus socket path short; \ + prefix=`mktemp --tmpdir --directory sssd-intg.XXXXXXXX`; \ + $(LN_S) "$$prefix" intg/pfx; \ + cd intg/bld; \ + $(abs_top_srcdir)/configure \ + --prefix="$$prefix" \ + --with-ldb-lib-dir="$$prefix"/lib/ldb \ + --enable-intgcheck-reqs \ + $(INTGCHECK_CONFIGURE_FLAGS); \ + $(MAKE) $(AM_MAKEFLAGS); \ + : Force single-thread install to workaround concurrency issues; \ + $(MAKE) $(AM_MAKEFLAGS) -j1 install; \ + : Remove .la files from LDB module directory to avoid loader warnings; \ + rm "$$prefix"/lib/ldb/*.la; \ + $(MAKE) $(AM_MAKEFLAGS) -C src/tests/intg intgcheck-installed; \ + cd ../..; \ + rm -Rf "$$prefix" intg + #################### # Client Libraries # #################### diff --git a/configure.ac b/configure.ac index 8d57c664b..a127d0bd5 100644 --- a/configure.ac +++ b/configure.ac @@ -175,6 +175,7 @@ m4_include([src/external/configlib.m4]) m4_include([src/external/libnfsidmap.m4]) m4_include([src/external/cwrap.m4]) m4_include([src/external/libresolv.m4]) +m4_include([src/external/intgcheck.m4]) if test x$build_config_lib = xyes; then m4_include([src/external/libaugeas.m4]) @@ -315,6 +316,8 @@ AM_CONDITIONAL([BUILD_PYTHON_BINDINGS], [test x"$with_python2_bindings" = xyes \ -o x"$with_python3_bindings" = xyes]) +AM_PYTHON2_MODULE([ldap]) + if test x$HAVE_SELINUX != x; then AM_CHECK_SELINUX AM_CHECK_SELINUX_LOGIN_DIR @@ -387,6 +390,8 @@ AM_CHECK_CMOCKA AM_CHECK_UID_WRAPPER AM_CHECK_NSS_WRAPPER +SSS_ENABLE_INTGCHECK_REQS + AM_CONDITIONAL([HAVE_DEVSHM], [test -d /dev/shm]) abs_build_dir=`pwd` @@ -396,6 +401,7 @@ AC_SUBST([abs_builddir], $abs_build_dir) AC_CONFIG_FILES([Makefile contrib/sssd.spec src/examples/rwtab src/doxy.config src/sysv/sssd src/sysv/gentoo/sssd src/sysv/SUSE/sssd po/Makefile.in src/man/Makefile src/tests/cwrap/Makefile + src/tests/intg/Makefile src/providers/ipa/ipa_hbac.pc src/providers/ipa/ipa_hbac.doxy src/lib/idmap/sss_idmap.pc src/lib/idmap/sss_idmap.doxy src/sss_client/sudo/sss_sudo.doxy diff --git a/contrib/ci/configure.sh b/contrib/ci/configure.sh index d5d4c791a..d68f33191 100644 --- a/contrib/ci/configure.sh +++ b/contrib/ci/configure.sh @@ -44,6 +44,7 @@ if [[ "$DISTRO_BRANCH" == -redhat-redhatenterprise*-7.*- ]]; then "--without-python3-bindings" ) fi + declare -r -a CONFIGURE_ARG_LIST fi # _CONFIGURE_SH diff --git a/contrib/ci/deps.sh b/contrib/ci/deps.sh index 4e0ce1e03..8f872037c 100644 --- a/contrib/ci/deps.sh +++ b/contrib/ci/deps.sh @@ -27,15 +27,23 @@ declare -a DEPS_LIST=( valgrind ) +# "Integration tests dependencies satisfied" flag +declare DEPS_INTGCHECK_SATISFIED=true + if [[ "$DISTRO_BRANCH" == -redhat-* ]]; then declare _DEPS_LIST_SPEC DEPS_LIST+=( clang-analyzer + fakeroot libcmocka-devel mock + nss_wrapper + openldap-clients + openldap-servers + pytest + python-ldap rpm-build uid_wrapper - nss_wrapper ) _DEPS_LIST_SPEC=` sed -e 's/@PACKAGE_VERSION@/0/g' \ @@ -98,6 +106,7 @@ if [[ "$DISTRO_BRANCH" == -debian-* ]]; then xml-core xsltproc ) + DEPS_INTGCHECK_SATISFIED=false fi declare -a -r DEPS_LIST diff --git a/contrib/ci/run b/contrib/ci/run index 2f81a002d..5f668ff7d 100755 --- a/contrib/ci/run +++ b/contrib/ci/run @@ -188,6 +188,7 @@ function build_debug() export CFLAGS="$DEBUG_CFLAGS" declare test_dir declare test_dir_distcheck + declare intgcheck_configure_args declare distcheck_configure_args declare status @@ -217,6 +218,14 @@ function build_debug() ((status == 0)) if "$MODERATE"; then + if "$DEPS_INTGCHECK_SATISFIED"; then + printf -v intgcheck_configure_args " %q" \ + "${CONFIGURE_ARG_LIST[@]}" + stage make-intgcheck make -j $CPU_NUM intgcheck \ + INTGCHECK_CONFIGURE_FLAGS=" \ + $intgcheck_configure_args" + fi + test_dir_distcheck=`mktemp --directory /dev/shm/ci-test-dir.XXXXXXXX` # Single thread due to https://fedorahosted.org/sssd/ticket/2354 status=0 diff --git a/src/external/cwrap.m4 b/src/external/cwrap.m4 index 0bd0bc9c9..b03d1ef00 100644 --- a/src/external/cwrap.m4 +++ b/src/external/cwrap.m4 @@ -4,20 +4,19 @@ dnl AM_CHECK_WRAPPER(name, conditional) dnl If the cwrap library is found, sets the HAVE_$name conditional AC_DEFUN([AM_CHECK_WRAPPER], [ - FOUND_WRAPPER=0 - AC_MSG_CHECKING([for $1]) PKG_CHECK_EXISTS([$1], [ AC_MSG_RESULT([yes]) - FOUND_WRAPPER=1 + AC_SUBST([$2], [yes]) ], [ AC_MSG_RESULT([no]) + AC_SUBST([$2], [no]) AC_MSG_WARN([cwrap library $1 not found, some tests will not run]) ]) - AM_CONDITIONAL($2, [ test x$FOUND_WRAPPER = x1]) + AM_CONDITIONAL($2, [ test x$2 = xyes]) ]) AC_DEFUN([AM_CHECK_UID_WRAPPER], diff --git a/src/external/intgcheck.m4 b/src/external/intgcheck.m4 new file mode 100644 index 000000000..80d41b599 --- /dev/null +++ b/src/external/intgcheck.m4 @@ -0,0 +1,32 @@ +AC_CHECK_PROG([HAVE_FAKEROOT], [fakeroot], [yes], [no]) + +AC_PATH_PROG([PYTEST], [py.test]) +AS_IF([test -n "$PYTEST"], [HAVE_PYTEST=yes], [HAVE_PYTEST=no]) + +dnl Check for variable and fail unless value is "yes" +dnl The second argument will be printed in error message in case of error +dnl Usage: +dnl SSS_INTGCHECK_REQ(variable, message) + +AC_DEFUN([SSS_INTGCHECK_REQ], [ + AS_IF([test x$$1 = xyes], , [ + AC_MSG_ERROR([cannot enable integration tests: $2 not found])]) +]) + +AC_DEFUN([SSS_ENABLE_INTGCHECK_REQS], [ + AC_ARG_ENABLE(intgcheck-reqs, + [AS_HELP_STRING([--enable-intgcheck-reqs], + [enable checking for integration test requirements [default=no]])], + [enable_intgcheck_reqs="$enableval"], + [enable_intgcheck_reqs="no"]) + if test x"$enable_intgcheck_reqs" = xyes; then + SSS_INTGCHECK_REQ([HAVE_UID_WRAPPER], [uid_wrapper]) + SSS_INTGCHECK_REQ([HAVE_NSS_WRAPPER], [nss_wrapper]) + SSS_INTGCHECK_REQ([HAVE_SLAPD], [slapd]) + SSS_INTGCHECK_REQ([HAVE_LDAPMODIFY], [ldapmodify]) + SSS_INTGCHECK_REQ([HAVE_FAKEROOT], [fakeroot]) + SSS_INTGCHECK_REQ([HAVE_PYTHON2], [python2]) + SSS_INTGCHECK_REQ([HAVE_PYTEST], [pytest]) + SSS_INTGCHECK_REQ([HAVE_PY2MOD_LDAP], [python-ldap]) + fi +]) diff --git a/src/external/ldap.m4 b/src/external/ldap.m4 index 3a99ddfcc..43a01efae 100644 --- a/src/external/ldap.m4 +++ b/src/external/ldap.m4 @@ -90,3 +90,7 @@ AC_CHECK_TYPE([LDAPDerefRes], CFLAGS=$SAVE_CFLAGS LIBS=$SAVE_LIBS +AC_PATH_PROG([SLAPD], [slapd], , + [$PATH$PATH_SEPARATOR/usr/sbin$PATH_SEPARATOR]) +AS_IF([test -n "$SLAPD"], [HAVE_SLAPD=yes], [HAVE_SLAPD=no]) +AC_CHECK_PROG([HAVE_LDAPMODIFY], [ldapmodify], [yes], [no]) diff --git a/src/tests/intg/Makefile.am b/src/tests/intg/Makefile.am new file mode 100644 index 000000000..9383e1120 --- /dev/null +++ b/src/tests/intg/Makefile.am @@ -0,0 +1,61 @@ +dist_noinst_DATA = \ + config.py.m4 \ + ds.py \ + ds_openldap.py \ + ent.py \ + ent_test.py \ + ldap_ent.py \ + ldap_test.py \ + util.py \ + $(NULL) + +config.py: config.py.m4 + m4 -D "prefix=\`$(prefix)'" \ + -D "sysconfdir=\`$(sysconfdir)'" \ + -D "dbpath=\`$(dbpath)'" \ + -D "pidpath=\`$(pidpath)'" \ + -D "logpath=\`$(logpath)'" \ + -D "mcpath=\`$(mcpath)'" \ + $< > $@ + +root: + : "Create directory for emulated root's D-Bus cookies." + : "See http://dbus.freedesktop.org/doc/dbus-specification.html#auth-mechanisms" + $(MKDIR_P) -m 0700 root/.dbus-keyrings + +passwd: root + echo "root:x:0:0:root:$(abs_builddir)/root:/bin/bash" > $@ + +group: + echo "root:x:0:" > $@ + +CLEANFILES=config.py config.pyc passwd group + +clean-local: + rm -Rf root + +intgcheck-installed: config.py passwd group + pipepath="$(DESTDIR)$(pipepath)"; \ + if test $${#pipepath} -gt 80; then \ + echo "error: Pipe directory path too long," \ + "D-Bus won't be able to open sockets" >&2; \ + exit 1; \ + fi + set -e; \ + cd "$(abs_srcdir)"; \ + nss_wrapper=$$(pkg-config --libs nss_wrapper); \ + uid_wrapper=$$(pkg-config --libs uid_wrapper); \ + PATH="$$(dirname -- $(SLAPD)):$$PATH" \ + PATH="$(DESTDIR)$(sbindir):$(DESTDIR)$(bindir):$$PATH" \ + PATH="$(abs_builddir):$(abs_srcdir):$$PATH" \ + PYTHONPATH="$(abs_builddir):$(abs_srcdir)" \ + LDB_MODULES_PATH="$(DESTDIR)$(ldblibdir)" \ + LD_PRELOAD="$$nss_wrapper $$uid_wrapper" \ + NSS_WRAPPER_PASSWD="$(abs_builddir)/passwd" \ + NSS_WRAPPER_GROUP="$(abs_builddir)/group" \ + NSS_WRAPPER_MODULE_SO_PATH="$(DESTDIR)$(nsslibdir)/libnss_sss.so.2" \ + NSS_WRAPPER_MODULE_FN_PREFIX="sss" \ + UID_WRAPPER=1 \ + UID_WRAPPER_ROOT=1 \ + fakeroot $(PYTHON2) $(PYTEST) -v --tb=native $(INTGCHECK_PYTEST_ARGS) . + rm -f $(DESTDIR)$(logpath)/* diff --git a/src/tests/intg/config.py.m4 b/src/tests/intg/config.py.m4 new file mode 100644 index 000000000..563127c6e --- /dev/null +++ b/src/tests/intg/config.py.m4 @@ -0,0 +1,13 @@ +""" +Build configuration variables. +""" + +PREFIX = "prefix" +SYSCONFDIR = "sysconfdir" +SSSDCONFDIR = SYSCONFDIR + "/sssd" +CONF_PATH = SSSDCONFDIR + "/sssd.conf" +DB_PATH = "dbpath" +PID_PATH = "pidpath" +PIDFILE_PATH = PID_PATH + "/sssd.pid" +LOG_PATH = "logpath" +MCACHE_PATH = "mcpath" diff --git a/src/tests/intg/ds.py b/src/tests/intg/ds.py new file mode 100644 index 000000000..a87190275 --- /dev/null +++ b/src/tests/intg/ds.py @@ -0,0 +1,61 @@ +# +# Abstract directory server instance class +# +# Copyright (c) 2015 Red Hat, Inc. +# Author: Nikolai Kondrashov <Nikolai.Kondrashov@redhat.com> +# +# This is free software; you can redistribute it and/or modify it +# under the terms of the GNU General Public License as published by +# the Free Software Foundation; version 2 only +# +# This program 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 +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +# + +import ldap + +class DS: + """Abstract directory server instance.""" + + def __init__(self, dir, port, base_dn, admin_rdn, admin_pw): + """ + Initialize the instance. + + Arguments: + dir Path to the root of the filesystem hierarchy to create + the instance under. + port TCP port on localhost to bind the server to. + base_dn Base DN. + admin_rdn Administrator DN, relative to BASE_DN. + admin_pw Administrator password. + """ + self.dir = dir + self.port = port + self.ldap_url = "ldap://localhost:" + str(self.port) + self.base_dn = base_dn + self.admin_rdn = admin_rdn + self.admin_dn = admin_rdn + "," + base_dn + self.admin_pw = admin_pw + + def setup(self): + """Setup the instance""" + raise NotImplementedError() + + def teardown(self): + """Teardown the instance""" + raise NotImplementedError() + + def bind(self): + """Connect to the server and bind as admin, return connection.""" + conn = ldap.initialize(self.ldap_url) + conn.simple_bind_s(self.admin_dn, self.admin_pw) + return conn + + def __del__(self): + """Destroy the instance.""" + self.teardown() diff --git a/src/tests/intg/ds_openldap.py b/src/tests/intg/ds_openldap.py new file mode 100644 index 000000000..c58e53a2a --- /dev/null +++ b/src/tests/intg/ds_openldap.py @@ -0,0 +1,279 @@ +# +# OpenLDAP directory server instance class +# +# Copyright (c) 2015 Red Hat, Inc. +# Author: Nikolai Kondrashov <Nikolai.Kondrashov@redhat.com> +# +# This is free software; you can redistribute it and/or modify it +# under the terms of the GNU General Public License as published by +# the Free Software Foundation; version 2 only +# +# This program 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 +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +# + +import hashlib +import base64 +import urllib +import time +import ldap +import os +import errno +import signal +import shutil +import sys +from util import * +from ds import DS + +def hash_password(password): + """Generate userPassword value for a password.""" + salt = os.urandom(4) + hash = hashlib.sha1(password) + hash.update(salt) + return "{SSHA}" + base64.standard_b64encode(hash.digest() + salt) + +class DSOpenLDAP(DS): + """OpenLDAP directory server instance.""" + + def __init__(self, dir, port, base_dn, admin_rdn, admin_pw): + """ + Initialize the instance. + + Arguments: + dir Path to the root of the filesystem hierarchy to create + the instance under. + port TCP port on localhost to bind the server to. + base_dn Base DN. + admin_rdn Administrator DN, relative to BASE_DN. + admin_pw Administrator password. + """ + DS.__init__(self, dir, port, base_dn, admin_rdn, admin_pw) + self.run_dir = self.dir + "/var/run/ldap" + self.pid_path = self.run_dir + "/slapd.pid" + self.conf_dir = self.dir + "/etc/ldap" + self.conf_slapd_d_dir = self.conf_dir + "/slapd.d" + self.data_dir = self.dir + "/var/lib/ldap" + + def _setup_config(self): + """Setup the instance initial configuration.""" + dist_lib_dir = first_dir("/usr/lib/openldap", + "/usr/lib64/openldap", + "/usr/lib/ldap") + dist_conf_dir = first_dir("/etc/ldap", + "/etc/openldap") + args_file = self.run_dir + "/slapd.args" + admin_pw_hash = hash_password(self.admin_pw) + uid = os.geteuid() + gid = os.getegid() + + # + # Add configuration + # + config = unindent(""" + dn: cn=config + objectClass: olcGlobal + cn: config + olcPidFile: {self.pid_path} + olcArgsFile: {args_file} + # Read slapd.conf(5) for possible values + olcLogLevel: none + + # Frontend settings + dn: olcDatabase={{-1}}frontend,cn=config + objectClass: olcDatabaseConfig + objectClass: olcFrontendConfig + olcDatabase: {{-1}}frontend + # The maximum number of entries that is returned for + # a search operation + olcSizeLimit: 500 + # Allow unlimited access to local connection from the local root + olcAccess: {{0}}to * by dn.exact=gidNumber={gid}+uidNumber={uid}, + cn=peercred,cn=external,cn=auth manage by * break + # Allow unauthenticated read access for schema and + # base DN autodiscovery + olcAccess: {{1}}to dn.exact="" by * read + olcAccess: {{2}}to dn.base="cn=Subschema" by * read + + # Config db settings + dn: olcDatabase=config,cn=config + objectClass: olcDatabaseConfig + olcDatabase: config + # Allow unlimited access to local connection from the local root + olcAccess: to * by dn.exact=gidNumber={gid}+uidNumber={uid}, + cn=peercred,cn=external,cn=auth manage by * break + olcRootDN: {self.admin_rdn},cn=config + olcRootPW: {admin_pw_hash} + + # Load schemas + dn: cn=schema,cn=config + objectClass: olcSchemaConfig + cn: schema + + include: file://{dist_conf_dir}/schema/core.ldif + include: file://{dist_conf_dir}/schema/cosine.ldif + include: file://{dist_conf_dir}/schema/nis.ldif + include: file://{dist_conf_dir}/schema/inetorgperson.ldif + + # Load module + dn: cn=module{{0}},cn=config + objectClass: olcModuleList + cn: module{{0}} + olcModulePath: {dist_lib_dir} + olcModuleLoad: back_hdb + + # Set defaults for the backend + dn: olcBackend=hdb,cn=config + objectClass: olcBackendConfig + olcBackend: hdb + + # The database definition. + dn: olcDatabase=hdb,cn=config + objectClass: olcDatabaseConfig + objectClass: olcHdbConfig + olcDatabase: hdb + olcDbCheckpoint: 512 30 + olcLastMod: TRUE + olcSuffix: {self.base_dn} + olcDbDirectory: {self.data_dir} + olcRootDN: {self.admin_dn} + olcRootPW: {admin_pw_hash} + olcDbIndex: objectClass eq + olcDbIndex: cn,uid eq + olcDbIndex: uidNumber,gidNumber eq + olcDbIndex: member,memberUid eq + olcAccess: to attrs=userPassword,shadowLastChange + by self write + by anonymous auth + by * none + olcAccess: to dn.base="" by * read + olcAccess: to * + by * read + """).format(**locals()) + + slapadd = subprocess.Popen( + ["slapadd", "-F", self.conf_slapd_d_dir, "-b", "cn=config"], + stdin = subprocess.PIPE, close_fds = True + ) + slapadd.communicate(config) + if slapadd.returncode != 0: + raise Exception("Failed to add configuration with slapadd") + + # + # Add database config (example from distribution) + # + db_config = unindent(""" + # One 0.25 GB cache + set_cachesize 0 268435456 1 + + # Transaction Log settings + set_lg_regionmax 262144 + set_lg_bsize 2097152 + """) + db_config_file = open(self.data_dir + "/DB_CONFIG", "w") + db_config_file.write(db_config) + db_config_file.close() + + def setup(self): + """Setup the instance.""" + ldapi_socket = self.run_dir + "/ldapi" + ldapi_url = "ldapi://" + urllib.quote(ldapi_socket, "") + url_list = ldapi_url + " " + self.ldap_url + + os.makedirs(self.conf_slapd_d_dir) + os.makedirs(self.run_dir) + os.makedirs(self.data_dir) + + # + # Setup initial configuration + # + self._setup_config() + + # + # Start the daemon + # + if subprocess.call(["slapd", "-F", self.conf_slapd_d_dir, + "-h", url_list]) != 0: + raise Exception("Failed to start slapd") + + # + # Wait until it is available + # + attempt = 0 + while True: + try: + ldap_conn = ldap.initialize(ldapi_url) + ldap_conn.simple_bind_s(self.admin_rdn + ",cn=config", self.admin_pw) + ldap_conn.unbind_s() + ldap_conn = ldap.initialize(self.ldap_url) + ldap_conn.simple_bind_s(self.admin_dn, self.admin_pw) + ldap_conn.unbind_s() + break + except ldap.SERVER_DOWN: + pass + if ++attempt > 30: + raise Exception("Failed to start slapd") + time.sleep(1) + + # + # Relax requirement of member attribute presence in groupOfNames + # + modlist = [ + (ldap.MOD_DELETE, "olcObjectClasses", + "{7}( 2.5.6.9 NAME 'groupOfNames' " + "DESC 'RFC2256: a group of names (DNs)' SUP top " + "STRUCTURAL MUST ( member $ cn ) MAY ( businessCategory $ " + "seeAlso $ owner $ ou $ o $ description ) )"), + (ldap.MOD_ADD, "olcObjectClasses", + "{7}( 2.5.6.9 NAME 'groupOfNames' " + "DESC 'RFC2256: a group of names (DNs)' SUP top " + "STRUCTURAL MUST ( cn ) MAY ( member $ businessCategory $ " + "seeAlso $ owner $ ou $ o $ description ) )"), + ] + ldap_conn = ldap.initialize(ldapi_url) + ldap_conn.simple_bind_s(self.admin_rdn + ",cn=config", self.admin_pw) + ldap_conn.modify_s("cn={0}core,cn=schema,cn=config", modlist) + ldap_conn.unbind_s() + + # + # Add data + # + ldap_conn = ldap.initialize(self.ldap_url) + ldap_conn.simple_bind_s(self.admin_dn, self.admin_pw) + ldap_conn.add_s(self.base_dn, [ + ("objectClass", ["dcObject", "organization"]), + ("o", "Example Company"), + ]) + ldap_conn.add_s("cn=Manager," + self.base_dn, [ + ("objectClass", "organizationalRole"), + ]) + for ou in ("Users", "Groups", "Netgroups", "Services", "Policies"): + ldap_conn.add_s("ou=" + ou + "," + self.base_dn, [ + ("objectClass", ["top", "organizationalUnit"]), + ]) + ldap_conn.unbind_s() + + def teardown(self): + """Teardown the instance.""" + # Wait for slapd to stop + try: + pid_file = open(self.pid_path, "r") + try: + os.kill(int(pid_file.read()), signal.SIGTERM) + finally: + pid_file.close() + attempt = 0 + while os.path.isfile(self.pid_path): + if ++attempt > 30: + raise Exception("Failed to stop slapd") + time.sleep(1) + except IOError, e: + if e.errno != errno.ENOENT: + raise + + for path in (self.conf_slapd_d_dir, self.run_dir, self.data_dir): + shutil.rmtree(path, True) diff --git a/src/tests/intg/ent.py b/src/tests/intg/ent.py new file mode 100644 index 000000000..13feaaf04 --- /dev/null +++ b/src/tests/intg/ent.py @@ -0,0 +1,470 @@ +# +# Abstract passwd/group entry management +# +# Copyright (c) 2015 Red Hat, Inc. +# Author: Nikolai Kondrashov <Nikolai.Kondrashov@redhat.com> +# +# This is free software; you can redistribute it and/or modify it +# under the terms of the GNU General Public License as published by +# the Free Software Foundation; version 2 only +# +# This program 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 +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +# + +from pprint import pformat +import pwd +import grp + +_PASSWD_LIST_DESC = {None: ("user", {})} +_GROUP_DESC = {"mem": ("member list", {None: ("member", {})})} +_GROUP_LIST_DESC = {None: ("group", _GROUP_DESC)} + +def _get_desc(desc_map, key): + """ + Get an item description from a container description map. + + Arguments: + desc_map Container description map. + key Item key, None for wildcard description. + """ + assert isinstance(desc_map, dict) + if key in desc_map: + return desc_map[key] + if None in desc_map: + desc = desc_map[None] + if key != None: + desc = (desc[0] + " " + pformat(key), desc[1]) + return desc + elif key == None: + return ("item", {}) + else: + return (pformat(key), {}) + +def _diff(ent, pattern, desc_map={}): + """ + Describe difference between an entry and a pattern. + Return None, if none. + + Arguments: + ent Entry. + pattern Pattern. + desc_map Container pattern description map. + + An entry is a value, a list of entries, or a dictionary of entries. + Entries are used to store passwd and group database entries as + dictionaries, in lists and dictionaries. + + A pattern is a value, a tuple, a list, or a dictionary of patterns. + + E.g. 123, "abc", [ 123, "abc" ], { "abc": 123 }, { "abc": ( 123 ) } + + A pattern can be matched against a value, a list, or a dictionary entry. + + A value is considered matching, if it's equal to the pattern. + + E.g. 123 == 123, 123 != 456, "abc" == "abc", "abc" != "def", 123 != "abc" + + A list is considered matching a pattern, if the pattern is a list or a + tuple, where each of pattern list items matches an entry list item and + vice versa, or where each pattern tuple item matches an entry list item, + but not necessarily the other way around. + + E.g. [] != "abc", [] == [], [ "abc", 123 ] == [ 123, "abc" ], + [ "abc" ] != [ 123 ], [ 123 ] != [], + [] == (), [ "abc", 123 ] == ( 123, "abc" ), + [ "abc" ] != ( 123 ), [ 123 ] == (), [ 123, "abc" ] == ( 123 ) + + NOTE: For the sake of readability, it is recommended to use + "contains_only" function to create patterns matching all entry list + items (list patterns), and "contains" function to create patterns + matching a subset of entry list items (tuple patterns). + + A dictionary is considered matching a pattern, if it is also a dictionary, + and all of pattern values match identically-keyed values of the + dictionary. + + E.g. {} == {}, {} != "abc", { "abc": 123, "def": 456 } == { "abc": 123 }, + { "abc": 123 } == {} + + Container pattern description map is a dictionary with keys being item + keys/indices and values being (name, description map) tuples. None key + points to a wildcard description, others to specific item descriptions. + The description map argument is optional, and is used to generate more + readable difference explanations. + """ + assert isinstance(desc_map, dict) + + if isinstance(pattern, dict): + if not isinstance(ent, dict): + return "not a dict, " + str(type(ent)) + + for key, value in pattern.iteritems(): + item_name, item_map = _get_desc(desc_map, key) + d = _diff(ent[key], value, item_map) + if d: + return item_name + " mismatch: " + d + elif isinstance(pattern, tuple): + if not isinstance(ent, list): + return "not a list, " + str(type(ent)) + + pattern_matches = [0 for pv in pattern] + + for ei, ev in enumerate(ent): + for pi, pv in enumerate(pattern): + d = _diff(ev, pv) + if not d: + pattern_matches[pi] += 1 + + unmatched_pattern = [ pattern[pi] for pi in \ + xrange(0, len(pattern)) \ + if pattern_matches[pi] == 0 ] + + items = _get_desc(desc_map, None)[0] + "s" + if len(unmatched_pattern) > 0: + return "\nexpected " + items + " not found:\n" + \ + pformat(unmatched_pattern) + elif isinstance(pattern, list): + if not isinstance(ent, list): + return "not a list, " + str(type(ent)) + + pattern_matches = [0 for pv in pattern] + ent_matches = [0 for ev in ent] + + for ei, ev in enumerate(ent): + for pi, pv in enumerate(pattern): + d = _diff(ev, pv) + if not d: + pattern_matches[pi] += 1 + ent_matches[ei] += 1 + + unmatched_pattern = [ pattern[pi] for pi in \ + xrange(0, len(pattern)) \ + if pattern_matches[pi] == 0 ] + unmatched_ent = [ ent[pi] for pi in \ + xrange(0, len(ent)) \ + if ent_matches[pi] == 0 ] + + items = _get_desc(desc_map, None)[0] + "s" + d = "" + if len(unmatched_pattern) > 0: + d += "\nexpected " + items + " not found:\n" + \ + pformat(unmatched_pattern) + if len(unmatched_ent) != 0: + d += "\nunexpected " + items + " found:\n" + \ + pformat(unmatched_ent) + if len(d) > 0: + return d + else: + if pattern != ent: + return pformat(pattern) + " != " + pformat(ent) + + return None + +def contains_only(*args): + """ + Produce a pattern matching all list items against arguments. + Use this function instead of constructing bare lists, for readability. + """ + return list(args) + +def contains(*args): + """ + Produce a pattern matching a subset of list items against arguments. + Use this function instead of constructing bare tuples, for readability. + """ + return args + +def _convert_passwd(passwd): + """ + Convert a passwd entry returned by pwd module to an entry dictionary. + """ + return dict( + name = passwd.pw_name, + passwd = passwd.pw_passwd, + uid = passwd.pw_uid, + gid = passwd.pw_gid, + gecos = passwd.pw_gecos, + dir = passwd.pw_dir, + shell = passwd.pw_shell + ) + +def get_passwd_by_name(name): + """Get a passwd database entry by name.""" + return _convert_passwd(pwd.getpwnam(name)) + +def get_passwd_by_uid(uid): + """Get a passwd database entry by UID.""" + return _convert_passwd(pwd.getpwuid(uid)) + +def assert_passwd_by_name(name, pattern): + """Assert a passwd entry, retrieved by name, matches a pattern.""" + try: + ent = get_passwd_by_name(name) + except KeyError, err: + assert False, err + d = _diff(ent, pattern) + assert not d, d + +def assert_passwd_by_uid(uid, pattern): + """Assert a passwd entry, retrieved by UID, matches a pattern.""" + try: + ent = get_passwd_by_uid(uid) + except KeyError, err: + assert False, err + d = _diff(ent, pattern) + assert not d, d + +def get_passwd_list(): + """Get passwd database entry list with root user removed.""" + passwd_list = pwd.getpwall() + for i, v in enumerate(passwd_list): + if v.pw_name == "root" and v.pw_uid == 0 and v.pw_gid == 0: + del passwd_list[i] + return map(_convert_passwd, passwd_list) + raise Exception("no root user found") + +def assert_passwd_list(pattern): + """Assert retrieved passwd list matches a pattern.""" + d = _diff(get_passwd_list(), pattern, _PASSWD_LIST_DESC) + assert not d, d + +def _diff_each_passwd_by_name(pattern_dict): + """ + Describe difference between each pattern_dict value and a passwd entry + retrieved by name being the corresponding key. + """ + try: + ent = dict((k, get_passwd_by_name(k)) for k in pattern_dict.keys()) + except KeyError, err: + return str(err) + return _diff(ent, pattern_dict, _PASSWD_LIST_DESC) + +def _diff_each_passwd_by_uid(pattern_dict): + """ + Describe difference between each pattern_dict value and a passwd entry + retrieved by UID being the corresponding key. + """ + try: + ent = dict((k, get_passwd_by_uid(k)) for k in pattern_dict.keys()) + except KeyError, err: + return str(err) + return _diff(ent, pattern_dict, _PASSWD_LIST_DESC) + +def _diff_each_passwd_with_name(pattern_seq): + """ + Describe difference between each pattern in pattern_seq sequence and a + passwd entry retrieved by name being the pattern's "name" value. + """ + return _diff_each_passwd_by_name(dict((p["name"], p) for p in pattern_seq)) + +def _diff_each_passwd_with_uid(pattern_seq): + """ + Describe difference between each pattern in pattern_seq sequence and a + passwd entry retrieved by UID being the pattern's "uid" value. + """ + return _diff_each_passwd_by_uid(dict((p["uid"], p) for p in pattern_seq)) + +def assert_each_passwd_by_name(pattern_dict): + """ + Assert each pattern_dict value matches a passwd entry retrieved by + name being the corresponding key. + """ + d = _diff_each_passwd_by_name(pattern_dict) + assert not d, d + +def assert_each_passwd_by_uid(pattern_dict): + """ + Assert each pattern_dict value matches a passwd entry retrieved by + UID being the corresponding key. + """ + d = _diff_each_passwd_by_uid(pattern_dict) + assert not d, d + +def assert_each_passwd_with_name(pattern_seq): + """ + Assert each pattern in pattern_seq sequence matches a passwd entry + retrieved by name being the pattern's "name" value. + """ + d = _diff_each_passwd_with_name(pattern_seq) + assert not d, d + +def assert_each_passwd_with_uid(pattern_seq): + """ + Assert each pattern in pattern_seq sequence matches a passwd entry + retrieved by UID being the pattern's "uid" value. + """ + d = _diff_each_passwd_with_uid(pattern_seq) + assert not d, d + +def _diff_passwd(pattern): + """ + Describe difference between passwd database and a pattern. + Each pattern entry must have "name" and "uid" attribute. + """ + d = _diff(get_passwd_list(), pattern, _PASSWD_LIST_DESC) + if d: + return "list mismatch: " + d + d = _diff_each_passwd_with_name(pattern) + if d: + return "name retrieval mismatch: " + d + d = _diff_each_passwd_with_uid(pattern) + if d: + return "UID retrieval mismatch: " + d + return None + +def assert_passwd(pattern): + """ + Assert passwd database matches a pattern. + Each pattern entry must have "name" and "uid" attribute. + """ + d = _diff_passwd(pattern) + assert not d, d + +def _convert_group(group): + """ + Convert a group entry returned by grp module to an entry dictionary. + """ + return dict( + name = group.gr_name, + passwd = group.gr_passwd, + gid = group.gr_gid, + mem = group.gr_mem + ) + +def get_group_by_name(name): + """Get a group database entry by name.""" + return _convert_group(grp.getgrnam(name)) + +def get_group_by_gid(gid): + """Get a group database entry by GID.""" + return _convert_group(grp.getgrgid(gid)) + +def assert_group_by_name(name, pattern): + """Assert a group entry, retrieved by name, matches a pattern.""" + try: + ent = get_group_by_name(name) + except KeyError, err: + assert False, err + d = _diff(ent, pattern, _GROUP_DESC) + assert not d, d + +def assert_group_by_gid(gid, pattern): + """Assert a group entry, retrieved by GID, matches a pattern.""" + try: + ent = get_group_by_gid(gid) + except KeyError, err: + assert False, err + d = _diff(ent, pattern, _GROUP_DESC) + assert not d, d + +def get_group_list(): + """Get group database entry list with root group removed.""" + group_list = grp.getgrall() + for i, v in enumerate(group_list): + if v.gr_name == "root" and v.gr_gid == 0: + del group_list[i] + return map(_convert_group, group_list) + raise Exception("no root group found") + +def assert_group_list(pattern): + """Assert retrieved group list matches a pattern.""" + d = _diff(get_group_list(), pattern, _GROUP_LIST_DESC) + assert not d, d + +def _diff_each_group_by_name(pattern_dict): + """ + Describe difference between each pattern_dict value and a group entry + retrieved by name being the corresponding key. + """ + try: + ent = dict((k, get_group_by_name(k)) for k in pattern_dict.keys()) + except KeyError, err: + return str(err) + return _diff(ent, pattern_dict, _GROUP_LIST_DESC) + +def _diff_each_group_by_gid(pattern_dict): + """ + Describe difference between each pattern_dict value and a group entry + retrieved by GID being the corresponding key. + """ + try: + ent = dict((k, get_group_by_gid(k)) for k in pattern_dict.keys()) + except KeyError, err: + return str(err) + return _diff(ent, pattern_dict, _GROUP_LIST_DESC) + +def _diff_each_group_with_name(pattern_seq): + """ + Describe difference between each pattern in pattern_seq sequence and a + group entry retrieved name being the pattern's "name" value. + """ + return _diff_each_group_by_name(dict((p["name"], p) for p in pattern_seq)) + +def _diff_each_group_with_gid(pattern_seq): + """ + Describe difference between each pattern in pattern_seq sequence and a + group entry retrieved by GID being the pattern's "gid" value. + """ + return _diff_each_group_by_gid(dict((p["gid"], p) for p in pattern_seq)) + +def assert_each_group_by_name(pattern_dict): + """ + Assert each pattern_dict value matches a group entry retrieved by + name being the corresponding key. + """ + d = _diff_each_group_by_name(pattern_dict) + assert not d, d + +def assert_each_group_by_gid(pattern_dict): + """ + Assert each pattern_dict value matches a group entry retrieved by + GID being the corresponding key. + """ + d = _diff_each_group_by_gid(pattern_dict) + assert not d, d + +def assert_each_group_with_name(pattern_seq): + """ + Assert each pattern in pattern_seq sequence matches a group entry + retrieved by name being the pattern's "name" value. + """ + d = _diff_each_group_with_name(pattern_seq) + assert not d, d + +def assert_each_group_with_gid(pattern_seq): + """ + Assert each pattern in pattern_seq sequence matches a group entry + retrieved by GID being the pattern's "gid" value. + """ + d = _diff_each_group_with_gid(pattern_seq) + assert not d, d + +def _diff_group(pattern): + """ + Describe difference between group database and a pattern. + Each pattern entry must have "name" and "gid" attribute. + """ + d = _diff(get_group_list(), pattern, _GROUP_LIST_DESC) + if d: + return "list mismatch: " + d + d = _diff_each_group_with_name(pattern) + if d: + return "name retrieval mismatch: " + d + d = _diff_each_group_with_gid(pattern) + if d: + return "GID retrieval mismatch: " + d + return None + +def assert_group(pattern): + """ + Assert group database matches a pattern. + Each pattern entry must have "name" and "gid" attribute. + """ + d = _diff_group(pattern) + assert not d, d diff --git a/src/tests/intg/ent_test.py b/src/tests/intg/ent_test.py new file mode 100644 index 000000000..896fcbe14 --- /dev/null +++ b/src/tests/intg/ent_test.py @@ -0,0 +1,417 @@ +# +# ent.py module tests +# +# Copyright (c) 2015 Red Hat, Inc. +# Author: Nikolai Kondrashov <Nikolai.Kondrashov@redhat.com> +# +# This is free software; you can redistribute it and/or modify it +# under the terms of the GNU General Public License as published by +# the Free Software Foundation; version 2 only +# +# This program 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 +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +# +import re +import os +import io +import shutil +import pytest +import ent +from util import * + +def backup_envvar_file(name): + path = os.environ[name] + backup_path = path + ".bak" + shutil.copyfile(path, backup_path) + return path + +def restore_envvar_file(name): + path = os.environ[name] + backup_path = path + ".bak" + os.rename(backup_path, path) + +@pytest.fixture(scope="module") +def passwd_path(request): + name = "NSS_WRAPPER_PASSWD" + request.addfinalizer(lambda: restore_envvar_file(name)) + return backup_envvar_file(name) + +@pytest.fixture(scope="module") +def group_path(request): + name = "NSS_WRAPPER_GROUP" + request.addfinalizer(lambda: restore_envvar_file(name)) + return backup_envvar_file(name) + +USER1 = dict(name="user1", passwd="x", uid=1001, gid=2001, + gecos="User 1", dir="/home/user1", shell="/bin/bash") +USER2 = dict(name="user2", passwd="x", uid=1002, gid=2002, + gecos="User 2", dir="/home/user2", shell="/bin/bash") +USER_LIST = [USER1, USER2] +USER_NAME_DICT = dict((u["name"], u) for u in USER_LIST) +USER_UID_DICT = dict((u["uid"], u) for u in USER_LIST) + + +EMPTY_GROUP = dict(name="empty_group", passwd="x", gid=2000, + mem=ent.contains_only()) +GROUP1 = dict(name="group1", passwd="x", gid=2001, + mem=ent.contains_only()) +GROUP2 = dict(name="group2", passwd="x", gid=2002, + mem=ent.contains_only()) +ONE_USER_GROUP1 = dict(name="one_user_group1", passwd="x", gid=2011, + mem=ent.contains_only("user1")) +ONE_USER_GROUP2 = dict(name="one_user_group2", passwd="x", gid=2012, + mem=ent.contains_only("user2")) +TWO_USER_GROUP = dict(name="two_user_group", passwd="x", gid=2020, + mem=ent.contains_only("user1", "user2")) +GROUP_LIST = [EMPTY_GROUP, + GROUP1, + GROUP2, + ONE_USER_GROUP1, + ONE_USER_GROUP2, + TWO_USER_GROUP] +GROUP_NAME_DICT = dict((g["name"], g) for g in GROUP_LIST) +GROUP_GID_DICT = dict((g["gid"], g) for g in GROUP_LIST) + +@pytest.fixture(scope="module") +def users_and_groups(request, passwd_path, group_path): + passwd_contents = "".join([ + "{name}:{passwd}:{uid}:{gid}:{gecos}:{dir}:{shell}\n".format(**u) \ + for u in USER_LIST + ]) + group_contents = "".join([ + "%s:%s:%s:%s\n" % (g["name"], g["passwd"], g["gid"], + ",".join(g["mem"])) \ + for g in GROUP_LIST + ]) + + with open(passwd_path, "a") as f: + f.write(passwd_contents) + with open(group_path, "a") as f: + f.write(group_contents) + +def test_assert_passwd_by_name(users_and_groups): + ent.assert_passwd_by_name("user1", {}) + ent.assert_passwd_by_name("user1", dict(name="user1", uid=1001)) + ent.assert_passwd_by_name("user1", USER1) + + try: + ent.assert_passwd_by_name("user3", {}) + assert False + except AssertionError, e: + assert str(e) == "'getpwnam(): name not found: user3'" + + try: + ent.assert_passwd_by_name("user2", dict(name="user1")) + assert False + except AssertionError, e: + assert str(e) == "'name' mismatch: 'user1' != 'user2'" + +def test_assert_passwd_by_uid(users_and_groups): + ent.assert_passwd_by_uid(1001, {}) + ent.assert_passwd_by_uid(1001, dict(name="user1", uid=1001)) + ent.assert_passwd_by_uid(1001, USER1) + + try: + ent.assert_passwd_by_uid(1003, {}) + assert False + except AssertionError, e: + assert str(e) == "'getpwuid(): uid not found: 1003'" + + try: + ent.assert_passwd_by_uid(1002, dict(name="user1")) + assert False + except AssertionError, e: + assert str(e) == "'name' mismatch: 'user1' != 'user2'" + + +def test_assert_passwd_list(users_and_groups): + ent.assert_passwd_list(ent.contains()) + ent.assert_passwd_list(ent.contains(USER1)) + ent.assert_passwd_list(ent.contains_only(*USER_LIST)) + try: + ent.assert_passwd_list(ent.contains_only()) + assert False + except AssertionError, e: + assert not re.search("expected users not found:", str(e)) + assert re.search("unexpected users found:", str(e)) + try: + ent.assert_passwd_list(ent.contains(dict(name="non_existent"))) + assert False + except AssertionError, e: + assert re.search("expected users not found:", str(e)) + assert not re.search("unexpected users found:", str(e)) + +def test_assert_each_passwd_by_name(users_and_groups): + ent.assert_each_passwd_by_name({}) + ent.assert_each_passwd_by_name(dict(user1=USER1)) + ent.assert_each_passwd_by_name(USER_NAME_DICT) + try: + ent.assert_each_passwd_by_name(dict(user3={})) + assert False + except AssertionError, e: + assert str(e) == "'getpwnam(): name not found: user3'" + try: + ent.assert_each_passwd_by_name(dict(user1=dict(name="user2"))) + assert False + except AssertionError, e: + assert str(e) == \ + "user 'user1' mismatch: 'name' mismatch: 'user2' != 'user1'" + +def test_assert_each_passwd_by_uid(users_and_groups): + ent.assert_each_passwd_by_uid({}) + ent.assert_each_passwd_by_uid({1001:USER1}) + ent.assert_each_passwd_by_uid(USER_UID_DICT) + try: + ent.assert_each_passwd_by_uid({1003:{}}) + assert False + except AssertionError, e: + assert str(e) == "'getpwuid(): uid not found: 1003'" + try: + ent.assert_each_passwd_by_uid({1001:dict(uid=1002)}) + assert False + except AssertionError, e: + assert str(e) == \ + "user 1001 mismatch: 'uid' mismatch: 1002 != 1001" + +def test_assert_each_passwd_with_name(users_and_groups): + ent.assert_each_passwd_with_name([]) + ent.assert_each_passwd_with_name([USER1]) + ent.assert_each_passwd_with_name(USER_LIST) + try: + ent.assert_each_passwd_with_name([dict(name="user3")]) + assert False + except AssertionError, e: + assert str(e) == "'getpwnam(): name not found: user3'" + try: + ent.assert_each_passwd_with_name([dict(name="user1", uid=1002)]) + assert False + except AssertionError, e: + assert str(e) == \ + "user 'user1' mismatch: 'uid' mismatch: 1002 != 1001" + +def test_assert_each_passwd_with_uid(users_and_groups): + ent.assert_each_passwd_with_uid([]) + ent.assert_each_passwd_with_uid([USER1]) + ent.assert_each_passwd_with_uid(USER_LIST) + try: + ent.assert_each_passwd_with_uid([dict(uid=1003)]) + assert False + except AssertionError, e: + assert str(e) == "'getpwuid(): uid not found: 1003'" + try: + ent.assert_each_passwd_with_uid([dict(name="user2", uid=1001)]) + assert False + except AssertionError, e: + assert str(e) == \ + "user 1001 mismatch: 'name' mismatch: 'user2' != 'user1'" + +def test_assert_passwd(users_and_groups): + ent.assert_passwd(ent.contains()) + ent.assert_passwd(ent.contains(USER1)) + ent.assert_passwd(ent.contains_only(*USER_LIST)) + try: + ent.assert_passwd(ent.contains(dict(name="user3", uid=1003))) + assert False + except AssertionError, e: + assert re.search("list mismatch:", str(e)) + assert re.search("expected users not found:", str(e)) + assert not re.search("unexpected users found:", str(e)) + try: + ent.assert_passwd(ent.contains_only(USER1)) + assert False + except AssertionError, e: + assert re.search("list mismatch:", str(e)) + assert not re.search("expected users not found:", str(e)) + assert re.search("unexpected users found:", str(e)) + +def test_group_member_matching(users_and_groups): + ent.assert_group_by_name("empty_group", dict(mem=ent.contains())) + ent.assert_group_by_name("empty_group", dict(mem=ent.contains_only())) + try: + ent.assert_group_by_name("empty_group", + dict(mem=ent.contains("user1"))) + except AssertionError, e: + assert re.search("member list mismatch:", str(e)) + assert re.search("expected members not found:", str(e)) + + ent.assert_group_by_name("one_user_group1", dict(mem=ent.contains())) + ent.assert_group_by_name("one_user_group1", + dict(mem=ent.contains("user1"))) + ent.assert_group_by_name("one_user_group1", + dict(mem=ent.contains_only("user1"))) + try: + ent.assert_group_by_name("one_user_group1", + dict(mem=ent.contains_only())) + except AssertionError, e: + assert re.search("member list mismatch:", str(e)) + assert re.search("unexpected members found:", str(e)) + assert not re.search("expected members not found:", str(e)) + try: + ent.assert_group_by_name("one_user_group1", + dict(mem=ent.contains_only("user3"))) + except AssertionError, e: + assert re.search("member list mismatch:", str(e)) + assert re.search("unexpected members found:", str(e)) + assert re.search("expected members not found:", str(e)) + try: + ent.assert_group_by_name("one_user_group1", + dict(mem=ent.contains("user3"))) + except AssertionError, e: + assert re.search("member list mismatch:", str(e)) + assert not re.search("unexpected members found:", str(e)) + assert re.search("expected members not found:", str(e)) + + ent.assert_group_by_name("two_user_group", dict(mem=ent.contains())) + ent.assert_group_by_name("two_user_group", + dict(mem=ent.contains("user1"))) + ent.assert_group_by_name("two_user_group", + dict(mem=ent.contains("user1", "user2"))) + ent.assert_group_by_name("two_user_group", + dict(mem=ent.contains_only("user1", "user2"))) + try: + ent.assert_group_by_name("two_user_group", + dict(mem=ent.contains_only("user1"))) + except AssertionError, e: + assert re.search("member list mismatch:", str(e)) + assert re.search("unexpected members found:", str(e)) + assert not re.search("expected members not found:", str(e)) + +def test_assert_group_by_name(users_and_groups): + ent.assert_group_by_name("group1", {}) + ent.assert_group_by_name("group1", dict(name="group1", gid=2001)) + ent.assert_group_by_name("group1", GROUP1) + + try: + ent.assert_group_by_name("group3", {}) + assert False + except AssertionError, e: + assert str(e) == "'getgrnam(): name not found: group3'" + + try: + ent.assert_group_by_name("group2", dict(name="group1")) + assert False + except AssertionError, e: + assert str(e) == "'name' mismatch: 'group1' != 'group2'" + +def test_assert_group_by_gid(users_and_groups): + ent.assert_group_by_gid(2001, {}) + ent.assert_group_by_gid(2001, dict(name="group1", gid=2001)) + ent.assert_group_by_gid(2001, GROUP1) + + try: + ent.assert_group_by_gid(2003, {}) + assert False + except AssertionError, e: + assert str(e) == "'getgrgid(): gid not found: 2003'" + + try: + ent.assert_group_by_gid(2002, dict(name="group1")) + assert False + except AssertionError, e: + assert str(e) == "'name' mismatch: 'group1' != 'group2'" + + +def test_assert_group_list(users_and_groups): + ent.assert_group_list(ent.contains()) + ent.assert_group_list(ent.contains(GROUP1)) + ent.assert_group_list(ent.contains_only(*GROUP_LIST)) + try: + ent.assert_group_list(ent.contains_only()) + assert False + except AssertionError, e: + assert not re.search("expected groups not found:", str(e)) + assert re.search("unexpected groups found:", str(e)) + try: + ent.assert_group_list(ent.contains(dict(name="non_existent"))) + assert False + except AssertionError, e: + assert re.search("expected groups not found:", str(e)) + assert not re.search("unexpected groups found:", str(e)) + +def test_assert_each_group_by_name(users_and_groups): + ent.assert_each_group_by_name({}) + ent.assert_each_group_by_name(dict(group1=GROUP1)) + ent.assert_each_group_by_name(GROUP_NAME_DICT) + try: + ent.assert_each_group_by_name(dict(group3={})) + assert False + except AssertionError, e: + assert str(e) == "'getgrnam(): name not found: group3'" + try: + ent.assert_each_group_by_name(dict(group1=dict(name="group2"))) + assert False + except AssertionError, e: + assert str(e) == "group 'group1' mismatch: " + \ + "'name' mismatch: 'group2' != 'group1'" + +def test_assert_each_group_by_gid(users_and_groups): + ent.assert_each_group_by_gid({}) + ent.assert_each_group_by_gid({2001:GROUP1}) + ent.assert_each_group_by_gid(GROUP_GID_DICT) + try: + ent.assert_each_group_by_gid({2003:{}}) + assert False + except AssertionError, e: + assert str(e) == "'getgrgid(): gid not found: 2003'" + try: + ent.assert_each_group_by_gid({2001:dict(gid=2002)}) + assert False + except AssertionError, e: + assert str(e) == \ + "group 2001 mismatch: 'gid' mismatch: 2002 != 2001" + +def test_assert_each_group_with_name(users_and_groups): + ent.assert_each_group_with_name([]) + ent.assert_each_group_with_name([GROUP1]) + ent.assert_each_group_with_name(GROUP_LIST) + try: + ent.assert_each_group_with_name([dict(name="group3")]) + assert False + except AssertionError, e: + assert str(e) == "'getgrnam(): name not found: group3'" + try: + ent.assert_each_group_with_name([dict(name="group1", gid=2002)]) + assert False + except AssertionError, e: + assert str(e) == \ + "group 'group1' mismatch: 'gid' mismatch: 2002 != 2001" + +def test_assert_each_group_with_gid(users_and_groups): + ent.assert_each_group_with_gid([]) + ent.assert_each_group_with_gid([GROUP1]) + ent.assert_each_group_with_gid(GROUP_LIST) + try: + ent.assert_each_group_with_gid([dict(gid=2003)]) + assert False + except AssertionError, e: + assert str(e) == "'getgrgid(): gid not found: 2003'" + try: + ent.assert_each_group_with_gid([dict(name="group2", gid=2001)]) + assert False + except AssertionError, e: + assert str(e) == \ + "group 2001 mismatch: 'name' mismatch: 'group2' != 'group1'" + +def test_assert_group(users_and_groups): + ent.assert_group(ent.contains()) + ent.assert_group(ent.contains(GROUP1)) + ent.assert_group(ent.contains_only(*GROUP_LIST)) + try: + ent.assert_group(ent.contains(dict(name="group3", gid=2003))) + assert False + except AssertionError, e: + assert re.search("list mismatch:", str(e)) + assert re.search("expected groups not found:", str(e)) + assert not re.search("unexpected groups found:", str(e)) + try: + ent.assert_group(ent.contains_only(GROUP1)) + assert False + except AssertionError, e: + assert re.search("list mismatch:", str(e)) + assert not re.search("expected groups not found:", str(e)) + assert re.search("unexpected groups found:", str(e)) diff --git a/src/tests/intg/ldap_ent.py b/src/tests/intg/ldap_ent.py new file mode 100644 index 000000000..ef2d14727 --- /dev/null +++ b/src/tests/intg/ldap_ent.py @@ -0,0 +1,102 @@ +# +# LDAP modlist generation +# +# Copyright (c) 2015 Red Hat, Inc. +# Author: Nikolai Kondrashov <Nikolai.Kondrashov@redhat.com> +# +# This is free software; you can redistribute it and/or modify it +# under the terms of the GNU General Public License as published by +# the Free Software Foundation; version 2 only +# +# This program 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 +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +# + +def user(base_dn, uid, uidNumber, gidNumber): + """ + Generate an RFC2307(bis) user add-modlist for passing to ldap.add* + """ + uidNumber = str(uidNumber) + gidNumber = str(gidNumber) + return ( + "uid=" + uid + ",ou=Users," + base_dn, + [ + ('objectClass', ['top', 'inetOrgPerson', 'posixAccount']), + ('cn', [uidNumber]), + ('sn', ['User']), + ('uidNumber', [uidNumber]), + ('gidNumber', [gidNumber]), + ('userPassword', ['Password' + uidNumber]), + ('homeDirectory', ['/home/' + uid]), + ('loginShell', ['/bin/bash']), + ] + ) + +def group(base_dn, cn, gidNumber, member_uids=[]): + """ + Generate an RFC2307 group add-modlist for passing to ldap.add*. + """ + gidNumber = str(gidNumber) + attr_list = [ + ('objectClass', ['top', 'posixGroup']), + ('gidNumber', [gidNumber]) + ] + if len(member_uids) > 0: + attr_list.append(('memberUid', member_uids)) + return ("cn=" + cn + ",ou=Groups," + base_dn, attr_list) + +def group_bis(base_dn, cn, gidNumber, member_uids=[], member_gids=[]): + """ + Generate an RFC2307bis group add-modlist for passing to ldap.add*. + """ + gidNumber = str(gidNumber) + attr_list = [ + ('objectClass', ['top', 'extensibleObject', 'groupOfNames']), + ('gidNumber', [gidNumber]) + ] + if len(member_uids) > 0: + attr_list.append( + ('member', [ + "uid=" + uid + ",ou=Users," + base_dn for + uid in member_uids + ]) + ) + if len(member_gids) > 0: + attr_list.append( + ('member', [ + "cn=" + gid + ",ou=Groups," + base_dn for + gid in member_gids + ]) + ) + return ("cn=" + cn + ",ou=Groups," + base_dn, attr_list) + +class List(list): + """LDAP add-modlist list""" + + def __init__(self, base_dn): + self.base_dn = base_dn + + def add_user(self, uid, uidNumber, gidNumber, + base_dn=None): + """Add an RFC2307(bis) user add-modlist.""" + self.append(user(base_dn or self.base_dn, + uid, uidNumber, gidNumber)) + + def add_group(self, cn, gidNumber, member_uids=[], + base_dn=None): + """Add an RFC2307 group add-modlist.""" + self.append(group(base_dn or self.base_dn, + cn, gidNumber, member_uids)) + + def add_group_bis(self, cn, gidNumber, + member_uids=[], member_gids=[], + base_dn=None): + """Add an RFC2307bis group add-modlist.""" + self.append(group_bis(base_dn or self.base_dn, + cn, gidNumber, + member_uids, member_gids)) diff --git a/src/tests/intg/ldap_test.py b/src/tests/intg/ldap_test.py new file mode 100644 index 000000000..afc77d702 --- /dev/null +++ b/src/tests/intg/ldap_test.py @@ -0,0 +1,261 @@ +# +# LDAP integration test +# +# Copyright (c) 2015 Red Hat, Inc. +# Author: Nikolai Kondrashov <Nikolai.Kondrashov@redhat.com> +# +# This is free software; you can redistribute it and/or modify it +# under the terms of the GNU General Public License as published by +# the Free Software Foundation; version 2 only +# +# This program 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 +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +# +import os +import sys +import stat +import pwd +import grp +import ent +import config +import signal +import subprocess +import time +import ldap +import pytest +import ds_openldap +import ldap_ent +from util import * + +LDAP_BASE_DN="dc=example,dc=com" + +@pytest.fixture(scope="module") +def ds_inst(request): + """LDAP server instance fixture""" + ds_inst = ds_openldap.DSOpenLDAP( + config.PREFIX, 10389, LDAP_BASE_DN, + "cn=admin", "Secret123") + try: + ds_inst.setup() + except: + ds_inst.teardown() + raise + request.addfinalizer(lambda: ds_inst.teardown()) + return ds_inst + +@pytest.fixture(scope="module") +def ldap_conn(request, ds_inst): + """LDAP server connection fixture""" + ldap_conn = ds_inst.bind() + ldap_conn.ds_inst = ds_inst + request.addfinalizer(lambda: ldap_conn.unbind_s()) + return ldap_conn + +def create_ldap_fixture(request, ldap_conn, ent_list): + """Add LDAP entries and add teardown for removing them""" + for entry in ent_list: + ldap_conn.add_s(entry[0], entry[1]) + def teardown(): + for entry in ent_list: + ldap_conn.delete_s(entry[0]) + request.addfinalizer(teardown) + +def create_conf_fixture(request, contents): + """Generate sssd.conf and add teardown for removing it""" + conf = open(config.CONF_PATH, "w") + conf.write(contents) + conf.close() + os.chmod(config.CONF_PATH, stat.S_IRUSR | stat.S_IWUSR) + request.addfinalizer(lambda: os.unlink(config.CONF_PATH)) + +def create_sssd_fixture(request): + """Start sssd and add teardown for stopping it and removing state""" + if subprocess.call(["sssd", "-D", "-f"]) != 0: + raise Exception("sssd start failed") + def teardown(): + try: + pid_file = open(config.PIDFILE_PATH, "r") + pid = int(pid_file.read()) + os.kill(pid, signal.SIGTERM) + while True: + try: + os.kill(pid, signal.SIGCONT) + except: + break + time.sleep(1) + except: + pass + for path in os.listdir(config.DB_PATH): + os.unlink(config.DB_PATH + "/" + path) + for path in os.listdir(config.MCACHE_PATH): + os.unlink(config.MCACHE_PATH + "/" + path) + request.addfinalizer(teardown) + +@pytest.fixture +def sanity_rfc2307(request, ldap_conn): + ent_list = ldap_ent.List(LDAP_BASE_DN) + ent_list.add_user("user1", 1001, 2001) + ent_list.add_user("user2", 1002, 2002) + ent_list.add_user("user3", 1003, 2003) + + ent_list.add_group("group1", 2001) + ent_list.add_group("group2", 2002) + ent_list.add_group("group3", 2003) + + ent_list.add_group("empty_group", 2010) + + ent_list.add_group("two_user_group", 2012, ["user1", "user2"]) + create_ldap_fixture(request, ldap_conn, ent_list) + + conf = unindent("""\ + [sssd] + debug_level = 0xffff + config_file_version = 2 + domains = LDAP + services = nss, pam + + [nss] + debug_level = 0xffff + memcache_timeout = 0 + + [pam] + debug_level = 0xffff + + [domain/LDAP] + ldap_auth_disable_tls_never_use_in_production = true + debug_level = 0xffff + enumerate = true + ldap_schema = rfc2307 + id_provider = ldap + auth_provider = ldap + sudo_provider = ldap + ldap_uri = {ldap_conn.ds_inst.ldap_url} + ldap_search_base = {ldap_conn.ds_inst.base_dn} + """).format(**locals()) + create_conf_fixture(request, conf) + create_sssd_fixture(request) + return None + +@pytest.fixture +def sanity_rfc2307_bis(request, ldap_conn): + ent_list = ldap_ent.List(LDAP_BASE_DN) + ent_list.add_user("user1", 1001, 2001) + ent_list.add_user("user2", 1002, 2002) + ent_list.add_user("user3", 1003, 2003) + + ent_list.add_group_bis("group1", 2001) + ent_list.add_group_bis("group2", 2002) + ent_list.add_group_bis("group3", 2003) + + ent_list.add_group_bis("empty_group1", 2010) + ent_list.add_group_bis("empty_group2", 2011) + + ent_list.add_group_bis("two_user_group", 2012, ["user1", "user2"]) + ent_list.add_group_bis("group_empty_group", 2013, [], ["empty_group1"]) + ent_list.add_group_bis("group_two_empty_groups", 2014, + [], ["empty_group1", "empty_group2"]) + ent_list.add_group_bis("one_user_group1", 2015, ["user1"]) + ent_list.add_group_bis("one_user_group2", 2016, ["user2"]) + ent_list.add_group_bis("group_one_user_group", 2017, + [], ["one_user_group1"]) + ent_list.add_group_bis("group_two_user_group", 2018, + [], ["two_user_group"]) + ent_list.add_group_bis("group_two_one_user_groups", 2019, + [], ["one_user_group1", "one_user_group2"]) + + create_ldap_fixture(request, ldap_conn, ent_list) + + conf = unindent("""\ + [sssd] + debug_level = 0xffff + config_file_version = 2 + domains = LDAP + services = nss, pam + + [nss] + debug_level = 0xffff + memcache_timeout = 0 + + [pam] + debug_level = 0xffff + + [domain/LDAP] + ldap_auth_disable_tls_never_use_in_production = true + debug_level = 0xffff + enumerate = true + ldap_schema = rfc2307bis + ldap_group_object_class = groupOfNames + id_provider = ldap + auth_provider = ldap + sudo_provider = ldap + ldap_uri = {ldap_conn.ds_inst.ldap_url} + ldap_search_base = {ldap_conn.ds_inst.base_dn} + """).format(**locals()) + create_conf_fixture(request, conf) + create_sssd_fixture(request) + return None + +def test_sanity_rfc2307(ldap_conn, sanity_rfc2307): + passwd_pattern = ent.contains_only( + dict(name='user1', passwd='*', uid=1001, gid=2001, gecos='1001', dir='/home/user1', shell='/bin/bash'), + dict(name='user2', passwd='*', uid=1002, gid=2002, gecos='1002', dir='/home/user2', shell='/bin/bash'), + dict(name='user3', passwd='*', uid=1003, gid=2003, gecos='1003', dir='/home/user3', shell='/bin/bash') + ) + ent.assert_passwd(passwd_pattern) + + group_pattern = ent.contains_only( + dict(name='group1', passwd='*', gid=2001, mem=ent.contains_only()), + dict(name='group2', passwd='*', gid=2002, mem=ent.contains_only()), + dict(name='group3', passwd='*', gid=2003, mem=ent.contains_only()), + dict(name='empty_group', passwd='*', gid=2010, mem=ent.contains_only()), + dict(name='two_user_group', passwd='*', gid=2012, mem=ent.contains_only("user1", "user2")) + ) + ent.assert_group(group_pattern) + + with pytest.raises(KeyError): + pwd.getpwnam("non_existent_user") + with pytest.raises(KeyError): + pwd.getpwuid(1) + with pytest.raises(KeyError): + grp.getgrnam("non_existent_group") + with pytest.raises(KeyError): + grp.getgrgid(1) + +def test_sanity_rfc2307_bis(ldap_conn, sanity_rfc2307_bis): + passwd_pattern = ent.contains_only( + dict(name='user1', passwd='*', uid=1001, gid=2001, gecos='1001', dir='/home/user1', shell='/bin/bash'), + dict(name='user2', passwd='*', uid=1002, gid=2002, gecos='1002', dir='/home/user2', shell='/bin/bash'), + dict(name='user3', passwd='*', uid=1003, gid=2003, gecos='1003', dir='/home/user3', shell='/bin/bash') + ) + ent.assert_passwd(passwd_pattern) + + group_pattern = ent.contains_only( + dict(name='group1', passwd='*', gid=2001, mem=ent.contains_only()), + dict(name='group2', passwd='*', gid=2002, mem=ent.contains_only()), + dict(name='group3', passwd='*', gid=2003, mem=ent.contains_only()), + dict(name='empty_group1', passwd='*', gid=2010, mem=ent.contains_only()), + dict(name='empty_group2', passwd='*', gid=2011, mem=ent.contains_only()), + dict(name='two_user_group', passwd='*', gid=2012, mem=ent.contains_only("user1", "user2")), + dict(name='group_empty_group', passwd='*', gid=2013, mem=ent.contains_only()), + dict(name='group_two_empty_groups', passwd='*', gid=2014, mem=ent.contains_only()), + dict(name='one_user_group1', passwd='*', gid=2015, mem=ent.contains_only("user1")), + dict(name='one_user_group2', passwd='*', gid=2016, mem=ent.contains_only("user2")), + dict(name='group_one_user_group', passwd='*', gid=2017, mem=ent.contains_only("user1")), + dict(name='group_two_user_group', passwd='*', gid=2018, mem=ent.contains_only("user1", "user2")), + dict(name='group_two_one_user_groups', passwd='*', gid=2019, mem=ent.contains_only("user1", "user2")) + ) + ent.assert_group(group_pattern) + + with pytest.raises(KeyError): + pwd.getpwnam("non_existent_user") + with pytest.raises(KeyError): + pwd.getpwuid(1) + with pytest.raises(KeyError): + grp.getgrnam("non_existent_group") + with pytest.raises(KeyError): + grp.getgrgid(1) diff --git a/src/tests/intg/util.py b/src/tests/intg/util.py new file mode 100644 index 000000000..5dd92b220 --- /dev/null +++ b/src/tests/intg/util.py @@ -0,0 +1,55 @@ +# +# Various functions +# +# Copyright (c) 2015 Red Hat, Inc. +# Author: Nikolai Kondrashov <Nikolai.Kondrashov@redhat.com> +# +# This is free software; you can redistribute it and/or modify it +# under the terms of the GNU General Public License as published by +# the Free Software Foundation; version 2 only +# +# This program 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 +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +# + +import re +import os +import subprocess + +UNINDENT_RE = re.compile("^ +", re.MULTILINE) + +def unindent(text): + """ + Unindent text by removing at most the number of spaces present in + the first non-empty line from the beginning of every line. + """ + indent_ref = [0] + def replace(match): + if indent_ref[0] == 0: + indent_ref[0] = len(match.group()) + return match.group()[indent_ref[0]:] + return UNINDENT_RE.sub(replace, text) + +def run_shell(): + """ + Execute an interactive shell under "screen", preserving environment. + For use as a breakpoint for debugging. + """ + subprocess.call([ + "screen", "-D", "-m", "bash", "-c", + "PATH='" + os.getenv("PATH", "") + "' " + + "LD_LIBRARY_PATH='" + os.getenv("LD_LIBRARY_PATH", "") + "' " + + "LD_PRELOAD='" + os.getenv("LD_PRELOAD", "") + "' " + + "bash -i" + ]) + +def first_dir(*args): + """Return first argument that points to an existing directory.""" + for arg in args: + if os.path.isdir(arg): + return arg |