Hi everyone,

I've worked on Samba integration tests for a while and here is where I got
thus far. I'll need to resume my work on Session Recording [1] for now and
will be happy if someone took the Samba work over. If not I'll return to it
some time in the future.

The attached patches add very rudimentary Samba AD DC instance setup and
teardown, and a very basic test, plus the requisite changes. They require
Samba built with AD DC support, which is missing from RHEL/Fedora repos, but
can conveniently be acquired from Lukas's COPR repo (Thanks, Lukas!):

    https://rhcopr-devel.lab.eng.brq.redhat.com/coprs/lslebodn/samba4/

The patches were confirmed working on Fedora 22 and Debian Testing. The latter
had resolv_wrapper installed manually, since it's not in Debian repos yet.

Among the things we still have to do are:

* Make Samba packages with AD DC support available for developers on
  Fedora/RHEL. Add instructions for getting them to contrib/ci/README.md and
  maybe somewhere else as well, if that's different from the already existing
  COPR repo.

* Add support for disabling SSSD netlink use, perhaps with an environment
  variable which can then be set in src/tests/intg/Makefile.am.
  See https://fedorahosted.org/sssd/ticket/2860

* Deliver resolv_wrapper to Debian package repos.

* Write the actual tests.

Thanks everyone who helped me get this far!

Hope we get this working and merged eventually :)

Nick

[1] http://spbnick.github.io/2015/10/26/open-source-session-recording.html
>From f4c15f0f055225d2b39dfb0d1b10ba89d5fccf97 Mon Sep 17 00:00:00 2001
From: Nikolai Kondrashov <[email protected]>
Date: Wed, 4 Nov 2015 19:49:42 +0200
Subject: [PATCH 1/6] Disable netlink for compatibility with socket_wrapper

Comment-out setup of route/address/link monitoring with netlink, as a
workaround for socket_wrapper not supporting netlink sockets. We need
socket_wrapper to setup Samba in integration tests.

A proper solution would be to add an option to disable netlink use in
SSSD, preferably at runtime. See https://fedorahosted.org/sssd/ticket/2860

The patch below fixes socket_wrapper (yay, a patch in a commit message!):

    diff --git a/src/socket_wrapper.c b/src/socket_wrapper.c
    index 9ba212b..a04a705 100644
    --- a/src/socket_wrapper.c
    +++ b/src/socket_wrapper.c
    @@ -2384,6 +2384,7 @@ static int swrap_socket(int family, int type, int protocol)
            case AF_INET6:
     #endif
                    break;
    +       case AF_NETLINK:
            case AF_UNIX:
                    return libc_socket(family, type, protocol);
            default:

However, it is unclear if this is a proper fix. It wasn't committed into the
official socket_wrapper repo yet.
---
 src/monitor/monitor.c | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/src/monitor/monitor.c b/src/monitor/monitor.c
index 89ac882..adc237d 100644
--- a/src/monitor/monitor.c
+++ b/src/monitor/monitor.c
@@ -2414,6 +2414,7 @@ static int monitor_process_init(struct mt_ctx *ctx,
         return ret;
     }
 
+#if 0
     ret = setup_netlink(ctx, ctx->ev, network_status_change_cb,
                         ctx, &ctx->nlctx);
     if (ret != EOK) {
@@ -2421,6 +2422,7 @@ static int monitor_process_init(struct mt_ctx *ctx,
               "Cannot set up listening for network notifications\n");
         return ret;
     }
+#endif
 
     /* start providers */
     num_providers = 0;
-- 
2.6.2

>From 0b76f0e7e94b3eefa69f46adc52e175689bf276c Mon Sep 17 00:00:00 2001
From: Nikolai Kondrashov <[email protected]>
Date: Wed, 4 Nov 2015 19:52:10 +0200
Subject: [PATCH 2/6] intg: Add socket_wrapper

Add socket_wrapper to integration tests setup. This is required for
Samba setup in integration tests.

Switch to using SYSV-version of fakeroot, "fakeroot-sysv", as
socket_wrapper doesn't support listening on unbound TCP sockets - a
thing which faked-tcp (a part of standard, TCP-version of fakeroot,
"fakeroot-tcp") does. A fix for this is available in socket_wrapper's
"master-fix" branch ATM, but using fakeroot-sysv seems a better idea
anyway.
---
 configure.ac               |  1 +
 contrib/ci/deps.sh         |  2 ++
 src/external/cwrap.m4      |  5 +++++
 src/external/intgcheck.m4  |  5 +++--
 src/tests/intg/Makefile.am | 15 +++++++++++----
 5 files changed, 22 insertions(+), 6 deletions(-)

diff --git a/configure.ac b/configure.ac
index c457879..836cb76 100644
--- a/configure.ac
+++ b/configure.ac
@@ -410,6 +410,7 @@ AM_CONDITIONAL([HAVE_CHECK], [test x$have_check != x])
 AM_CHECK_CMOCKA
 AM_CHECK_UID_WRAPPER
 AM_CHECK_NSS_WRAPPER
+AM_CHECK_SOCKET_WRAPPER
 
 SSS_ENABLE_INTGCHECK_REQS
 
diff --git a/contrib/ci/deps.sh b/contrib/ci/deps.sh
index c9a8a63..dc41ab3 100644
--- a/contrib/ci/deps.sh
+++ b/contrib/ci/deps.sh
@@ -43,6 +43,7 @@ if [[ "$DISTRO_BRANCH" == -redhat-* ]]; then
         pytest
         python-ldap
         rpm-build
+        socket_wrapper
         uid_wrapper
     )
     _DEPS_LIST_SPEC=`
@@ -109,6 +110,7 @@ if [[ "$DISTRO_BRANCH" == -debian-* ]]; then
         libssl-dev
         fakeroot
         libnss-wrapper
+        libsocket-wrapper
         libuid-wrapper
         python-pytest
         python-ldap
diff --git a/src/external/cwrap.m4 b/src/external/cwrap.m4
index b8489cc..1ee0934 100644
--- a/src/external/cwrap.m4
+++ b/src/external/cwrap.m4
@@ -28,3 +28,8 @@ AC_DEFUN([AM_CHECK_NSS_WRAPPER],
 [
     AM_CHECK_WRAPPER(nss_wrapper, HAVE_NSS_WRAPPER)
 ])
+
+AC_DEFUN([AM_CHECK_SOCKET_WRAPPER],
+[
+    AM_CHECK_WRAPPER(socket_wrapper, HAVE_SOCKET_WRAPPER)
+])
diff --git a/src/external/intgcheck.m4 b/src/external/intgcheck.m4
index 80d41b5..91bcde1 100644
--- a/src/external/intgcheck.m4
+++ b/src/external/intgcheck.m4
@@ -1,4 +1,4 @@
-AC_CHECK_PROG([HAVE_FAKEROOT], [fakeroot], [yes], [no])
+AC_CHECK_PROG([HAVE_FAKEROOT_SYSV], [fakeroot-sysv], [yes], [no])
 
 AC_PATH_PROG([PYTEST], [py.test])
 AS_IF([test -n "$PYTEST"], [HAVE_PYTEST=yes], [HAVE_PYTEST=no])
@@ -22,9 +22,10 @@ AC_DEFUN([SSS_ENABLE_INTGCHECK_REQS], [
     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_SOCKET_WRAPPER], [socket_wrapper])
         SSS_INTGCHECK_REQ([HAVE_SLAPD], [slapd])
         SSS_INTGCHECK_REQ([HAVE_LDAPMODIFY], [ldapmodify])
-        SSS_INTGCHECK_REQ([HAVE_FAKEROOT], [fakeroot])
+        SSS_INTGCHECK_REQ([HAVE_FAKEROOT_SYSV], [fakeroot-sysv])
         SSS_INTGCHECK_REQ([HAVE_PYTHON2], [python2])
         SSS_INTGCHECK_REQ([HAVE_PYTEST], [pytest])
         SSS_INTGCHECK_REQ([HAVE_PY2MOD_LDAP], [python-ldap])
diff --git a/src/tests/intg/Makefile.am b/src/tests/intg/Makefile.am
index 12a4fc2..efaf1b4 100644
--- a/src/tests/intg/Makefile.am
+++ b/src/tests/intg/Makefile.am
@@ -32,12 +32,16 @@ passwd: root
 group:
 	echo "root:x:0:" > $@
 
+sockets:
+	: Create directory for socket_wrapper sockets
+	mkdir sockets
+
 CLEANFILES=config.py config.pyc passwd group
 
 clean-local:
-	rm -Rf root
+	rm -Rf root sockets
 
-intgcheck-installed: config.py passwd group
+intgcheck-installed: config.py passwd group sockets
 	pipepath="$(DESTDIR)$(pipepath)"; \
 	if test $${#pipepath} -gt 80; then \
 	    echo "error: Pipe directory path too long," \
@@ -48,17 +52,20 @@ intgcheck-installed: config.py passwd group
 	cd "$(abs_srcdir)"; \
 	nss_wrapper=$$(pkg-config --libs nss_wrapper); \
 	uid_wrapper=$$(pkg-config --libs uid_wrapper); \
+	socket_wrapper=$$(pkg-config --libs socket_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" \
+	LD_PRELOAD="$$nss_wrapper $$uid_wrapper $$socket_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) .
+	SOCKET_WRAPPER_DEFAULT_IFACE=2 \
+	SOCKET_WRAPPER_DIR="$(abs_builddir)/sockets" \
+	    fakeroot-sysv $(PYTHON2) $(PYTEST) -v --tb=native $(INTGCHECK_PYTEST_ARGS) .
 	rm -f $(DESTDIR)$(logpath)/*
-- 
2.6.2

>From 166ad8a11240b227917d1235b41d4221ae705e7e Mon Sep 17 00:00:00 2001
From: Nikolai Kondrashov <[email protected]>
Date: Mon, 9 Nov 2015 15:45:11 +0200
Subject: [PATCH 3/6] intg: Add users and groups required by Samba

Add user "nobody" and groups "nobody" and "users" to passwd and group
files of base integration tests setup. This is required for Samba setup
in integration tests. Filter out those extra users and groups in ent.py
to keep existing and future tests simpler. Note that now filtering-out
doesn't complain if the users/groups in question are not there, which
might need to be re-considered.
---
 src/tests/intg/Makefile.am |  9 +++++++--
 src/tests/intg/ent.py      | 48 ++++++++++++++++++++++++++++++++--------------
 2 files changed, 41 insertions(+), 16 deletions(-)

diff --git a/src/tests/intg/Makefile.am b/src/tests/intg/Makefile.am
index efaf1b4..452ef2d 100644
--- a/src/tests/intg/Makefile.am
+++ b/src/tests/intg/Makefile.am
@@ -27,10 +27,15 @@ root:
 	$(MKDIR_P) -m 0700 root/.dbus-keyrings
 
 passwd: root
-	echo "root:x:0:0:root:$(abs_builddir)/root:/bin/bash" > $@
+	> $@
+	echo "root:x:0:0:root:$(abs_builddir)/root:/bin/bash" >> $@
+	echo "nobody:x:99:99:Nobody:/:/sbin/nologin" >> $@
 
 group:
-	echo "root:x:0:" > $@
+	> $@
+	echo "root:x:0:" >> $@
+	echo "nobody:x:99:" >> $@
+	echo "users:x:100:" >> $@
 
 sockets:
 	: Create directory for socket_wrapper sockets
diff --git a/src/tests/intg/ent.py b/src/tests/intg/ent.py
index 2d3d02a..48a5ded 100644
--- a/src/tests/intg/ent.py
+++ b/src/tests/intg/ent.py
@@ -182,6 +182,19 @@ def contains(*args):
     return args
 
 
+def _filter_list(ent_list, pattern_list):
+    """
+    Remove all entries from ent_list matching at least one pattern from
+    pattern_list, return ent_list.
+    """
+    for i in xrange(len(ent_list) - 1, -1, -1):
+        for p in pattern_list:
+            if _diff(ent_list[i], p) is None:
+                del ent_list[i]
+                break
+    return ent_list
+
+
 def _convert_passwd(passwd):
     """
     Convert a passwd entry returned by pwd module to an entry dictionary.
@@ -227,14 +240,17 @@ def assert_passwd_by_uid(uid, pattern):
     assert not d, d
 
 
+# A list of patterns matching preset users the module should ignore
+_PASSWD_BASE_PATTERN_LIST = [
+    dict(name="root", uid=0, gid=0),
+    dict(name="nobody", uid=99, gid=99)
+]
+
+
 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")
+    """Get passwd database entry list with preset entries removed."""
+    return _filter_list(map(_convert_passwd, pwd.getpwall()),
+                        _PASSWD_BASE_PATTERN_LIST)
 
 
 def assert_passwd_list(pattern):
@@ -387,14 +403,18 @@ def assert_group_by_gid(gid, pattern):
     assert not d, d
 
 
+# A list of patterns matching preset groups the module should ignore
+_GROUP_BASE_PATTERN_LIST = [
+    dict(name="root", gid=0),
+    dict(name="nobody", gid=99),
+    dict(name="users", gid=100),
+]
+
+
 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")
+    """Get group database entry list with preset entries removed."""
+    return _filter_list(map(_convert_group, grp.getgrall()),
+                        _GROUP_BASE_PATTERN_LIST)
 
 
 def assert_group_list(pattern):
-- 
2.6.2

>From 4ef190a0089d04cee33815dd5a7c5b14acd5ccda Mon Sep 17 00:00:00 2001
From: Nikolai Kondrashov <[email protected]>
Date: Mon, 9 Nov 2015 15:45:51 +0200
Subject: [PATCH 4/6] intg: Add nss_wrapper hosts file and hostname

Add "hosts" file and hostname override to nss_wrapper setup in
integration tests. This seems required for Samba setup, but wasn't
tested thoroughly.
---
 src/tests/intg/Makefile.am | 11 +++++++++--
 1 file changed, 9 insertions(+), 2 deletions(-)

diff --git a/src/tests/intg/Makefile.am b/src/tests/intg/Makefile.am
index 452ef2d..321cadb 100644
--- a/src/tests/intg/Makefile.am
+++ b/src/tests/intg/Makefile.am
@@ -41,12 +41,17 @@ sockets:
 	: Create directory for socket_wrapper sockets
 	mkdir sockets
 
-CLEANFILES=config.py config.pyc passwd group
+hosts:
+	> $@
+	echo "127.0.0.1 localhost" >> $@
+	echo "127.0.0.2 dc.samba.example.com dc" >> $@
+
+CLEANFILES=config.py config.pyc passwd group hosts
 
 clean-local:
 	rm -Rf root sockets
 
-intgcheck-installed: config.py passwd group sockets
+intgcheck-installed: config.py passwd group hosts sockets
 	pipepath="$(DESTDIR)$(pipepath)"; \
 	if test $${#pipepath} -gt 80; then \
 	    echo "error: Pipe directory path too long," \
@@ -66,6 +71,8 @@ intgcheck-installed: config.py passwd group sockets
 	LD_PRELOAD="$$nss_wrapper $$uid_wrapper $$socket_wrapper" \
 	NSS_WRAPPER_PASSWD="$(abs_builddir)/passwd" \
 	NSS_WRAPPER_GROUP="$(abs_builddir)/group" \
+	NSS_WRAPPER_HOSTS="$(abs_builddir)/hosts" \
+	NSS_WRAPPER_HOSTNAME="dc.samba.example.com" \
 	NSS_WRAPPER_MODULE_SO_PATH="$(DESTDIR)$(nsslibdir)/libnss_sss.so.2" \
 	NSS_WRAPPER_MODULE_FN_PREFIX="sss" \
 	UID_WRAPPER=1 \
-- 
2.6.2

>From 0c13d9cd28fb53b1036e45cd1521f12e64fba2fd Mon Sep 17 00:00:00 2001
From: Nikolai Kondrashov <[email protected]>
Date: Tue, 10 Nov 2015 15:40:38 +0200
Subject: [PATCH 5/6] intg: Add resolv_wrapper

Add resolv_wrapper to integration tests setup. This is required for
Samba setup in integration tests, for things such as KDC discovery.
Note that this breaks name resolution in tests which don't setup Samba
(as DNS server becomes unreachable) and thus they're switched to using
IP addresses instead of hostnames. Namely in the directory server setup.
---
 configure.ac               |  1 +
 contrib/ci/deps.sh         |  3 ++-
 src/external/cwrap.m4      |  5 +++++
 src/external/intgcheck.m4  |  1 +
 src/tests/intg/Makefile.am | 12 +++++++++---
 src/tests/intg/ds.py       |  2 +-
 6 files changed, 19 insertions(+), 5 deletions(-)

diff --git a/configure.ac b/configure.ac
index 836cb76..dadae20 100644
--- a/configure.ac
+++ b/configure.ac
@@ -411,6 +411,7 @@ AM_CHECK_CMOCKA
 AM_CHECK_UID_WRAPPER
 AM_CHECK_NSS_WRAPPER
 AM_CHECK_SOCKET_WRAPPER
+AM_CHECK_RESOLV_WRAPPER
 
 SSS_ENABLE_INTGCHECK_REQS
 
diff --git a/contrib/ci/deps.sh b/contrib/ci/deps.sh
index dc41ab3..8913f7b 100644
--- a/contrib/ci/deps.sh
+++ b/contrib/ci/deps.sh
@@ -42,6 +42,7 @@ if [[ "$DISTRO_BRANCH" == -redhat-* ]]; then
         openldap-servers
         pytest
         python-ldap
+        resolv_wrapper
         rpm-build
         socket_wrapper
         uid_wrapper
@@ -117,7 +118,7 @@ if [[ "$DISTRO_BRANCH" == -debian-* ]]; then
         ldap-utils
         slapd
     )
-    DEPS_INTGCHECK_SATISFIED=true
+    DEPS_INTGCHECK_SATISFIED=false
 fi
 
 declare -a -r DEPS_LIST
diff --git a/src/external/cwrap.m4 b/src/external/cwrap.m4
index 1ee0934..cb24f9f 100644
--- a/src/external/cwrap.m4
+++ b/src/external/cwrap.m4
@@ -33,3 +33,8 @@ AC_DEFUN([AM_CHECK_SOCKET_WRAPPER],
 [
     AM_CHECK_WRAPPER(socket_wrapper, HAVE_SOCKET_WRAPPER)
 ])
+
+AC_DEFUN([AM_CHECK_RESOLV_WRAPPER],
+[
+    AM_CHECK_WRAPPER(resolv_wrapper, HAVE_RESOLV_WRAPPER)
+])
diff --git a/src/external/intgcheck.m4 b/src/external/intgcheck.m4
index 91bcde1..3877cdf 100644
--- a/src/external/intgcheck.m4
+++ b/src/external/intgcheck.m4
@@ -23,6 +23,7 @@ AC_DEFUN([SSS_ENABLE_INTGCHECK_REQS], [
         SSS_INTGCHECK_REQ([HAVE_UID_WRAPPER], [uid_wrapper])
         SSS_INTGCHECK_REQ([HAVE_NSS_WRAPPER], [nss_wrapper])
         SSS_INTGCHECK_REQ([HAVE_SOCKET_WRAPPER], [socket_wrapper])
+        SSS_INTGCHECK_REQ([HAVE_RESOLV_WRAPPER], [resolv_wrapper])
         SSS_INTGCHECK_REQ([HAVE_SLAPD], [slapd])
         SSS_INTGCHECK_REQ([HAVE_LDAPMODIFY], [ldapmodify])
         SSS_INTGCHECK_REQ([HAVE_FAKEROOT_SYSV], [fakeroot-sysv])
diff --git a/src/tests/intg/Makefile.am b/src/tests/intg/Makefile.am
index 321cadb..72181c1 100644
--- a/src/tests/intg/Makefile.am
+++ b/src/tests/intg/Makefile.am
@@ -46,12 +46,16 @@ hosts:
 	echo "127.0.0.1 localhost" >> $@
 	echo "127.0.0.2 dc.samba.example.com dc" >> $@
 
-CLEANFILES=config.py config.pyc passwd group hosts
+resolv.conf:
+	> $@
+	echo "nameserver 127.0.0.2" >> $@
+
+CLEANFILES=config.py config.pyc passwd group hosts resolv.conf
 
 clean-local:
 	rm -Rf root sockets
 
-intgcheck-installed: config.py passwd group hosts sockets
+intgcheck-installed: config.py passwd group hosts resolv.conf sockets
 	pipepath="$(DESTDIR)$(pipepath)"; \
 	if test $${#pipepath} -gt 80; then \
 	    echo "error: Pipe directory path too long," \
@@ -63,12 +67,13 @@ intgcheck-installed: config.py passwd group hosts sockets
 	nss_wrapper=$$(pkg-config --libs nss_wrapper); \
 	uid_wrapper=$$(pkg-config --libs uid_wrapper); \
 	socket_wrapper=$$(pkg-config --libs socket_wrapper); \
+	resolv_wrapper=$$(pkg-config --libs resolv_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 $$socket_wrapper" \
+	LD_PRELOAD="$$nss_wrapper $$uid_wrapper $$socket_wrapper $$resolv_wrapper" \
 	NSS_WRAPPER_PASSWD="$(abs_builddir)/passwd" \
 	NSS_WRAPPER_GROUP="$(abs_builddir)/group" \
 	NSS_WRAPPER_HOSTS="$(abs_builddir)/hosts" \
@@ -77,6 +82,7 @@ intgcheck-installed: config.py passwd group hosts sockets
 	NSS_WRAPPER_MODULE_FN_PREFIX="sss" \
 	UID_WRAPPER=1 \
 	UID_WRAPPER_ROOT=1 \
+	RESOLV_WRAPPER_CONF="$(abs_builddir)/resolv.conf" \
 	SOCKET_WRAPPER_DEFAULT_IFACE=2 \
 	SOCKET_WRAPPER_DIR="$(abs_builddir)/sockets" \
 	    fakeroot-sysv $(PYTHON2) $(PYTEST) -v --tb=native $(INTGCHECK_PYTEST_ARGS) .
diff --git a/src/tests/intg/ds.py b/src/tests/intg/ds.py
index df08fac..a56b5d8 100644
--- a/src/tests/intg/ds.py
+++ b/src/tests/intg/ds.py
@@ -37,7 +37,7 @@ class DS:
         """
         self.dir = dir
         self.port = port
-        self.ldap_url = "ldap://localhost:"; + str(self.port)
+        self.ldap_url = "ldap://127.0.0.1:"; + str(self.port)
         self.base_dn = base_dn
         self.admin_rdn = admin_rdn
         self.admin_dn = admin_rdn + "," + base_dn
-- 
2.6.2

>From 5566e5916bbda5891b981b5959e7338f90e6c918 Mon Sep 17 00:00:00 2001
From: Nikolai Kondrashov <[email protected]>
Date: Mon, 9 Nov 2015 17:34:07 +0200
Subject: [PATCH 6/6] intg: Add a sketch of Samba AD DC tests

Add a sketch of Samba AD DC integration module and tests.

The module allows setting up and tearing down a Samba AD DC instance in
the specified root directory, bound to specified interfaces, with
specified realm, workgroup and admin password. It also provides
two functions for adding a user and a group.

The test sets up a DC, SSSD using it, adds a couple users and groups and
checks they're retrievable with NSS.

It checks only user/group names and group members, but not IDs or other
attributes, because these are set in a complicated way and it's not
clear yet whether and how we should be checking them. It also doesn't
check enumeration - that will likely need to account for the users and
groups Samba creates, likely by checking their presence and filtering
them out.
---
 contrib/ci/deps.sh         |   3 +
 src/external/intgcheck.m4  |   3 +
 src/tests/intg/Makefile.am |   2 +
 src/tests/intg/ad.py       | 271 +++++++++++++++++++++++++++++++++++++++++++++
 src/tests/intg/ad_test.py  | 185 +++++++++++++++++++++++++++++++
 src/tests/intg/util.py     |   6 +
 6 files changed, 470 insertions(+)
 create mode 100644 src/tests/intg/ad.py
 create mode 100644 src/tests/intg/ad_test.py

diff --git a/contrib/ci/deps.sh b/contrib/ci/deps.sh
index 8913f7b..174a688 100644
--- a/contrib/ci/deps.sh
+++ b/contrib/ci/deps.sh
@@ -44,6 +44,7 @@ if [[ "$DISTRO_BRANCH" == -redhat-* ]]; then
         python-ldap
         resolv_wrapper
         rpm-build
+        samba-dc
         socket_wrapper
         uid_wrapper
     )
@@ -91,6 +92,7 @@ if [[ "$DISTRO_BRANCH" == -debian-* ]]; then
         libpcre3-dev
         libpopt-dev
         libsasl2-dev
+        libsasl2-modules-gssapi-mit
         libselinux1-dev
         libsemanage1-dev
         libsmbclient-dev
@@ -104,6 +106,7 @@ if [[ "$DISTRO_BRANCH" == -debian-* ]]; then
         make
         python-dev
         python3-dev
+        samba
         samba-dev
         systemd
         xml-core
diff --git a/src/external/intgcheck.m4 b/src/external/intgcheck.m4
index 3877cdf..8b23547 100644
--- a/src/external/intgcheck.m4
+++ b/src/external/intgcheck.m4
@@ -1,5 +1,7 @@
 AC_CHECK_PROG([HAVE_FAKEROOT_SYSV], [fakeroot-sysv], [yes], [no])
 
+AC_CHECK_PROG([HAVE_SAMBA_TOOL], [samba-tool], [yes], [no])
+
 AC_PATH_PROG([PYTEST], [py.test])
 AS_IF([test -n "$PYTEST"], [HAVE_PYTEST=yes], [HAVE_PYTEST=no])
 
@@ -26,6 +28,7 @@ AC_DEFUN([SSS_ENABLE_INTGCHECK_REQS], [
         SSS_INTGCHECK_REQ([HAVE_RESOLV_WRAPPER], [resolv_wrapper])
         SSS_INTGCHECK_REQ([HAVE_SLAPD], [slapd])
         SSS_INTGCHECK_REQ([HAVE_LDAPMODIFY], [ldapmodify])
+        SSS_INTGCHECK_REQ([HAVE_SAMBA_TOOL], [samba-tool])
         SSS_INTGCHECK_REQ([HAVE_FAKEROOT_SYSV], [fakeroot-sysv])
         SSS_INTGCHECK_REQ([HAVE_PYTHON2], [python2])
         SSS_INTGCHECK_REQ([HAVE_PYTEST], [pytest])
diff --git a/src/tests/intg/Makefile.am b/src/tests/intg/Makefile.am
index 72181c1..b2c0ed3 100644
--- a/src/tests/intg/Makefile.am
+++ b/src/tests/intg/Makefile.am
@@ -1,4 +1,6 @@
 dist_noinst_DATA = \
+    ad.py \
+    ad_test.py \
     config.py.m4 \
     sssd_id.py \
     ds.py \
diff --git a/src/tests/intg/ad.py b/src/tests/intg/ad.py
new file mode 100644
index 0000000..4a32b82
--- /dev/null
+++ b/src/tests/intg/ad.py
@@ -0,0 +1,271 @@
+#
+# Abstract directory server instance class
+#
+# Copyright (c) 2015 Red Hat, Inc.
+# Author: Nikolai Kondrashov <[email protected]>
+#
+# 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 stat
+import time
+import errno
+import signal
+import shutil
+from util import *
+
+
+class AD:
+    """Samba AD DC instance."""
+
+    def __init__(self, dir, interfaces, realm, workgroup, 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.interfaces = interfaces
+        self.realm = realm
+        self.workgroup = workgroup
+        self.admin_pw = admin_pw
+
+        self.dir = dir
+        # NOTE: Only non-shared directories can appear here,
+        #       because they will be removed on teardown.
+        self.subdirs = Bunch(
+            config=Bunch(
+                path=dir + "/etc/samba", mode=0755),
+            ncalrpc=Bunch(
+                path=dir + "/var/run/samba/ncalrpc", mode=0755),
+            winbindd_socket=Bunch(
+                path=dir + "/var/run/samba/winbindd", mode=0755),
+            cache=Bunch(
+                path=dir + "/var/lib/samba/cache", mode=0755),
+            ntp_signd_socket=Bunch(
+                path=dir + "/var/lib/samba/ntp_signd", mode=0750),
+            private=Bunch(
+                path=dir + "/var/lib/samba/private", mode=0755),
+            state=Bunch(
+                path=dir + "/var/lib/samba/state", mode=0755),
+            winbindd_privileged_socket=Bunch(
+                path=dir + "/var/lib/samba/winbindd_privileged",
+                mode=0750),
+            log=Bunch(
+                path=dir + "/var/log/samba", mode=0755),
+            wtmp=Bunch(
+                path=dir + "/var/log/samba/wtmp", mode=0755),
+            pid=Bunch(
+                path=dir + "/var/run/samba", mode=0755),
+            lock=Bunch(
+                path=dir + "/var/run/samba/lock", mode=0755),
+            utmp=Bunch(
+                path=dir + "/var/run/samba/utmp", mode=0755),
+            printers=Bunch(
+                path=dir + "/var/spool/samba", mode=0755),
+            sysvol=Bunch(
+                path=dir + "/var/lib/samba/state/sysvol", mode=0755),
+            netlogon=Bunch(
+                path=dir + "/var/lib/samba/state/sysvol/scripts",
+                mode=0755),
+            home=Bunch(
+                path=dir + "/home/samba", mode=0755),
+        )
+        self.config_path = self.subdirs.config.path + "/smb.conf"
+        self.keytab_path = self.subdirs.config.path + "/krb5.keytab"
+        self.pid_path = self.subdirs.pid.path + "/samba.pid"
+
+    def setup(self):
+        """Setup the instance"""
+
+        # Create the directories
+        for subdir in self.subdirs.__dict__.itervalues():
+            try:
+                os.makedirs(subdir.path, subdir.mode)
+            except OSError as e:
+                if e.errno != errno.EEXIST:
+                    raise
+
+        # Generate configuration
+        config = unindent("""\
+            [global]
+                interfaces = {interfaces}
+                workgroup = {workgroup}
+                realm = {realm}
+                server role = active directory domain controller
+                security = USER
+                passdb backend = tdbsam
+                log file = {subdirs.log.path}/log.%m
+                private dir = {subdirs.private.path}
+                lock directory = {subdirs.lock.path}
+                state directory = {subdirs.state.path}
+                cache directory = {subdirs.cache.path}
+                pid directory = {subdirs.pid.path}
+                ntp signd socket directory = {subdirs.ntp_signd_socket.path}
+                utmp directory = {subdirs.utmp.path}
+                wtmp directory = {subdirs.wtmp.path}
+                ncalrpc dir = {subdirs.ncalrpc.path}
+                winbindd socket directory = {subdirs.winbindd_socket.path}
+                winbindd privileged socket directory = \
+                        {subdirs.winbindd_privileged_socket.path}
+                server services = +echo +smb -s3fs
+                dcerpc endpoint servers = +winreg +srvsvc
+                idmap_ldb:use rfc2307 = yes
+                posix:eadb = {subdirs.private.path}/eadb.tdb
+                xattr_tdb:file = {subdirs.private.path}/xattr.tdb
+                vfs objects = dfs_samba4 acl_xattr fake_acls \
+                              xattr_tdb streams_depot
+
+            [homes]
+                path = {subdirs.home.path}/%S
+                read only = No
+                browseable = No
+
+            [printers]
+                path = {subdirs.printers.path}
+                printable = Yes
+                browseable = No
+
+            [netlogon]
+                path = {subdirs.netlogon.path}
+                read only = No
+                guest ok = Yes
+
+            [sysvol]
+                path = {subdirs.sysvol.path}
+                read only = No
+        """).format(**self.__dict__)
+
+        config_file = open(self.config_path, "w")
+        config_file.write(config)
+        config_file.close()
+
+        env = os.environ.copy()
+        env["SELFTEST_WINBINDD_SOCKET_DIR"] = \
+            self.subdirs.winbindd_socket.path
+
+        # Provision the domain
+        argv = ["samba-tool", "domain", "provision",
+                "--use-rfc2307",
+                "--use-ntvfs",
+                "--configfile", self.config_path,
+                "--domain", self.workgroup,
+                "--server-role", "dc",
+                "--dns-backend", "SAMBA_INTERNAL",
+                "--adminpass", self.admin_pw,
+                "--krbtgtpass", self.admin_pw,
+                "--machinepass", self.admin_pw]
+        process = subprocess.Popen(argv, env=env)
+        process.communicate()
+        if process.wait() != 0:
+            raise Exception("Failed to provision the domain")
+
+        # Start Samba
+        argv = ["samba", "--configfile", self.config_path]
+        process = subprocess.Popen(argv, env=env)
+        process.communicate()
+        if process.wait() != 0:
+            raise Exception("Failed to start Samba")
+
+        # Export keytab for domain account
+        argv = ["samba-tool", "domain", "exportkeytab",
+                "--configfile", self.config_path,
+                "--principal", "dc$",
+                self.keytab_path]
+        process = subprocess.Popen(argv, env=env)
+        process.communicate()
+        if process.wait() != 0:
+            raise Exception("Failed to export the keytab")
+        os.chmod(self.keytab_path, stat.S_IRUSR | stat.S_IWUSR)
+
+        # Disable password complexity check
+        argv = ["samba-tool", "domain", "passwordsettings", "set",
+                "--configfile", self.config_path,
+                "--complexity=off",
+                "--history-length=0",
+                "--min-pwd-length=0",
+                "--min-pwd-age=0",
+                "--max-pwd-age=0"]
+        subprocess.check_call(argv)
+
+    def teardown(self):
+        """Teardown the instance"""
+        # Wait for Samba to stop
+        try:
+            while True:
+                pid_file = open(self.pid_path, "r")
+                try:
+                    pid = int(pid_file.read())
+                finally:
+                    pid_file.close()
+                os.kill(pid, signal.SIGTERM)
+                for attempt in range(1, 30):
+                    time.sleep(1)
+                    os.kill(pid, signal.SIGCONT)
+                raise Exception("Failed to stop Samba")
+        except IOError as e:
+            if e.errno != errno.ENOENT:
+                raise
+        except OSError as e:
+            if e.errno != errno.ESRCH:
+                raise
+
+        # Remove directories
+        for dir in self.subdirs.__dict__.itervalues():
+            shutil.rmtree(dir.path, True)
+
+    def __del__(self):
+        """Destroy the instance."""
+        self.teardown()
+
+    def add_user(self, username, uid=None, gid=None,
+                 password=None, gecos=None, dir=None, shell=None):
+        """Add a user"""
+        argv = ["samba-tool", "user", "add",
+                "--configfile", self.config_path,
+                "--home-directory",
+                "/home/" + username if dir is None else dir,
+                "--login-shell",
+                "/bin/bash" if shell is None else shell]
+        if uid is not None:
+            argv.extend(["--uid-number", uid])
+        if gid is not None:
+            argv.extend(["--gid-number", gid])
+        if gecos is not None:
+            argv.extend(["--gecos", gecos])
+        if password is None:
+            argv.append("--random-password")
+        argv.extend(["--", username])
+        if password is not None:
+            argv.append(password)
+        subprocess.check_call(argv)
+
+    def add_group(self, groupname, gid=None, members=[]):
+        """Add a group"""
+        argv = ["samba-tool", "group", "add",
+                "--configfile", self.config_path]
+        if gid is not None:
+            argv.extend(["--gid-number", gid])
+        argv.extend(["--", groupname])
+        subprocess.check_call(argv)
+        if len(members) != 0:
+            argv = ["samba-tool", "group", "addmembers",
+                    "--configfile", self.config_path,
+                    "--", groupname, ",".join(members)]
+            subprocess.check_call(argv)
diff --git a/src/tests/intg/ad_test.py b/src/tests/intg/ad_test.py
new file mode 100644
index 0000000..4ec57cf
--- /dev/null
+++ b/src/tests/intg/ad_test.py
@@ -0,0 +1,185 @@
+#
+# Samba integration test
+#
+# Copyright (c) 2015 Red Hat, Inc.
+# Author: Nikolai Kondrashov <[email protected]>
+#
+# 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 stat
+import pwd
+import grp
+import ent
+import config
+import signal
+import subprocess
+import pytest
+import ad
+from util import *
+
+
[email protected](scope="module")
+def ad_inst(request):
+    """AD instance fixture"""
+    interface_num = int(os.environ["SOCKET_WRAPPER_DEFAULT_IFACE"])
+    interfaces = \
+        "127.0.0.%u/8 fd00:0000:0000:0000:0000:0000:5357:5f%.2x/64" % \
+        (interface_num, interface_num)
+    ad_inst = ad.AD(
+        config.PREFIX, interfaces,
+        "SAMBA.EXAMPLE.COM", "SAMBA", "Secret123"
+    )
+    try:
+        ad_inst.setup()
+    except:
+        ad_inst.teardown()
+        raise
+    request.addfinalizer(lambda: ad_inst.teardown())
+    return ad_inst
+
+
+SCHEMA_RFC2307 = "rfc2307"
+SCHEMA_RFC2307_BIS = "rfc2307bis"
+
+
+def format_basic_conf(ad_inst, enum):
+    """Format a basic SSSD configuration"""
+    interface_num = int(os.environ["SOCKET_WRAPPER_DEFAULT_IFACE"])
+    ad_server = "127.0.0.%u" % (interface_num)
+    return unindent("""\
+        [sssd]
+        debug_level         = 0xffff
+        services = nss, pam
+        domains = samba.example.com
+
+        [nss]
+        debug_level         = 0xffff
+        memcache_timeout    = 0
+
+        [pam]
+        debug_level         = 0xffff
+
+        [domain/samba.example.com]
+        debug_level         = 0xffff
+        id_provider         = ad
+        access_provider     = ad
+        ldap_id_mapping     = true
+        ad_server           = {ad_server}
+        enumerate           = {enum}
+        krb5_keytab         = {ad_inst.keytab_path}
+    """).format(**locals())
+
+
+def create_conf_file(contents):
+    """Create sssd.conf with specified contents"""
+    conf = open(config.CONF_PATH, "w")
+    conf.write(contents)
+    conf.close()
+    os.chmod(config.CONF_PATH, stat.S_IRUSR | stat.S_IWUSR)
+
+
+def cleanup_conf_file():
+    """Remove sssd.conf, if it exists"""
+    if os.path.lexists(config.CONF_PATH):
+        os.unlink(config.CONF_PATH)
+
+
+def create_conf_cleanup(request):
+    """Add teardown for removing sssd.conf"""
+    request.addfinalizer(cleanup_conf_file)
+
+
+def create_conf_fixture(request, contents):
+    """
+    Create sssd.conf with specified contents and add teardown for removing it
+    """
+    create_conf_file(contents)
+    create_conf_cleanup(request)
+
+
+def create_sssd_process():
+    """Start the SSSD process"""
+    if subprocess.call(["sssd", "-D", "-f"]) != 0:
+        raise Exception("sssd start failed")
+
+
+def cleanup_sssd_process():
+    """Stop the SSSD process and remove its state"""
+    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
+    subprocess.call(["sss_cache", "-E"])
+    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)
+
+
+def create_sssd_cleanup(request):
+    """Add teardown for stopping SSSD and removing its state"""
+    request.addfinalizer(cleanup_sssd_process)
+
+
+def create_sssd_fixture(request):
+    """Start SSSD and add teardown for stopping it and removing its state"""
+    create_sssd_process()
+    create_sssd_cleanup(request)
+
+
[email protected]
+def sanity(request, ad_inst):
+    ad_inst.add_user("user1")
+    ad_inst.add_user("user2")
+    ad_inst.add_user("user3")
+    ad_inst.add_group("group1")
+    ad_inst.add_group("group2")
+    ad_inst.add_group("group3")
+    ad_inst.add_group("empty_group")
+    ad_inst.add_group("two_user_group", members=["user1", "user2"])
+    conf = format_basic_conf(ad_inst, enum=True)
+    create_conf_fixture(request, conf)
+    create_sssd_fixture(request)
+    return None
+
+
+def test_ad(sanity):
+    ent.assert_passwd_by_name("administrator", dict(name="administrator"))
+    ent.assert_group_by_name("domain admins",
+                             dict(name="domain admins",
+                                  mem=ent.contains_only("administrator")))
+    ent.assert_each_passwd_by_name(dict(user1={}, user2={}, user3={}))
+    ent.assert_each_group_by_name(dict(
+        group1={},
+        group2={},
+        group3={},
+        empty_group=dict(mem=ent.contains_only()),
+        two_user_group=dict(mem=ent.contains_only("user1", "user2"))
+    ))
+    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
index 66ec0ba..0df9fd4 100644
--- a/src/tests/intg/util.py
+++ b/src/tests/intg/util.py
@@ -22,6 +22,12 @@ import os
 import subprocess
 import config
 
+class Bunch:
+    """Dictionary with values accessible via dot notation"""
+    def __init__(self, **kwargs):
+        self.__dict__.update(kwargs)
+
+
 UNINDENT_RE = re.compile("^ +", re.MULTILINE)
 
 
-- 
2.6.2

_______________________________________________
sssd-devel mailing list
[email protected]
https://lists.fedorahosted.org/mailman/listinfo/sssd-devel

Reply via email to