tests: Add a sub-set of tests to show the restored DC is sound
authorTim Beale <timbeale@catalyst.net.nz>
Thu, 21 Jun 2018 03:04:00 +0000 (15:04 +1200)
committerAndrew Bartlett <abartlet@samba.org>
Tue, 3 Jul 2018 08:39:14 +0000 (10:39 +0200)
+ Add a new ldapcmp_restoredc.sh test that asserts that the original DC
backed up (backupfromdc) matches the new restored DC.
+ Add a new join_ldapcmp.sh test that asserts we can join a given DC,
and that the resulting DB matches the joined DC
+ Add a new login_basics.py test that sanity-checks Kerberos and NTLM
user login works. (This reuses the password_lockout base code, without
taking as long as the password_lockout tests do). Basic LDAP and SAMR
connections are also tested as a side-effect.
+ run the netlogonsvc test against the restored DC to prove we can
establish a netlogon connection.
+ run the same subset of rpc.echo tests that we do for RODC
+ run dbcheck over the new testenvs at the end of the test run

Signed-off-by: Tim Beale <timbeale@catalyst.net.nz>
Reviewed-by: Andrew Bartlett <abartlet@samba.org>
Reviewed-by: Gary Lockyer <gary@catalyst.net.nz>
source4/dsdb/tests/python/login_basics.py [new file with mode: 0755]
source4/selftest/tests.py
testprogs/blackbox/join_ldapcmp.sh [new file with mode: 0755]
testprogs/blackbox/ldapcmp_restoredc.sh [new file with mode: 0755]

diff --git a/source4/dsdb/tests/python/login_basics.py b/source4/dsdb/tests/python/login_basics.py
new file mode 100755 (executable)
index 0000000..d162788
--- /dev/null
@@ -0,0 +1,184 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+#
+# Basic sanity-checks of user login. This sanity-checks that a user can login
+# over both NTLM and Kerberos, that incorrect passwords are rejected, and that
+# the user can change their password successfully.
+#
+# Copyright Andrew Bartlett 2018
+#
+from __future__ import print_function
+import optparse
+import sys
+from samba.tests.subunitrun import TestProgram, SubunitOptions
+import samba.getopt as options
+from samba.auth import system_session
+from samba.credentials import MUST_USE_KERBEROS
+from samba.dsdb import UF_NORMAL_ACCOUNT
+from samba.samdb import SamDB
+from password_lockout_base import BasePasswordTestCase
+
+sys.path.insert(0, "bin/python")
+
+parser = optparse.OptionParser("password_lockout.py [options] <host>")
+sambaopts = options.SambaOptions(parser)
+parser.add_option_group(sambaopts)
+parser.add_option_group(options.VersionOptions(parser))
+# use command line creds if available
+credopts = options.CredentialsOptions(parser)
+parser.add_option_group(credopts)
+subunitopts = SubunitOptions(parser)
+parser.add_option_group(subunitopts)
+opts, args = parser.parse_args()
+
+if len(args) < 1:
+    parser.print_usage()
+    sys.exit(1)
+
+host = args[0]
+
+lp = sambaopts.get_loadparm()
+global_creds = credopts.get_credentials(lp)
+
+
+#
+# Tests start here
+#
+class BasicUserAuthTests(BasePasswordTestCase):
+
+    def setUp(self):
+        self.host = host
+        self.host_url = host_url
+        self.lp = lp
+        self.global_creds = global_creds
+        self.ldb = SamDB(url=self.host_url, credentials=self.global_creds,
+                         session_info=system_session(self.lp), lp=self.lp)
+        super(BasicUserAuthTests, self).setUp()
+
+    def _test_login_basics(self, creds):
+        username = creds.get_username()
+        userpass = creds.get_password()
+        userdn = "cn=%s,cn=users,%s" % (username, self.base_dn)
+        if creds.get_kerberos_state() == MUST_USE_KERBEROS:
+            logoncount_relation = 'greater'
+            lastlogon_relation = 'greater'
+            print("Performs a lockout attempt against LDAP using Kerberos")
+        else:
+            logoncount_relation = 'equal'
+            lastlogon_relation = 'equal'
+            print("Performs a lockout attempt against LDAP using NTLM")
+
+        # get the intial logon values for this user
+        res = self._check_account(userdn,
+                                  badPwdCount=0,
+                                  badPasswordTime=("greater", 0),
+                                  logonCount=(logoncount_relation, 0),
+                                  lastLogon=("greater", 0),
+                                  lastLogonTimestamp=("greater", 0),
+                                  userAccountControl=UF_NORMAL_ACCOUNT,
+                                  msDSUserAccountControlComputed=0,
+                                  msg='Initial test setup...')
+        badPasswordTime = int(res[0]["badPasswordTime"][0])
+        logonCount = int(res[0]["logonCount"][0])
+        lastLogon = int(res[0]["lastLogon"][0])
+        lastLogonTimestamp = int(res[0]["lastLogonTimestamp"][0])
+
+        test_creds = self.insta_creds(creds)
+
+        # check logging in with the wrong password fails
+        test_creds.set_password("thatsAcomplPASS1xBAD")
+        self.assertLoginFailure(self.host_url, test_creds, self.lp)
+        res = self._check_account(userdn,
+                                  badPwdCount=1,
+                                  badPasswordTime=("greater", badPasswordTime),
+                                  logonCount=logonCount,
+                                  lastLogon=lastLogon,
+                                  lastLogonTimestamp=lastLogonTimestamp,
+                                  userAccountControl=UF_NORMAL_ACCOUNT,
+                                  msDSUserAccountControlComputed=0,
+                                  msg='Test login with wrong password')
+        badPasswordTime = int(res[0]["badPasswordTime"][0])
+
+        # check logging in with the correct password succeeds
+        test_creds.set_password(userpass)
+        user_ldb = SamDB(url=self.host_url, credentials=test_creds, lp=self.lp)
+        res = self._check_account(userdn,
+                                  badPwdCount=0,
+                                  badPasswordTime=badPasswordTime,
+                                  logonCount=(logoncount_relation, logonCount),
+                                  lastLogon=('greater', lastLogon),
+                                  lastLogonTimestamp=lastLogonTimestamp,
+                                  userAccountControl=UF_NORMAL_ACCOUNT,
+                                  msDSUserAccountControlComputed=0,
+                                  msg='Test login with correct password')
+        logonCount = int(res[0]["logonCount"][0])
+        lastLogon = int(res[0]["lastLogon"][0])
+
+        # check that the user can change its password
+        new_password = "thatsAcomplPASS2"
+        user_ldb.modify_ldif("""
+dn: %s
+changetype: modify
+delete: userPassword
+userPassword: %s
+add: userPassword
+userPassword: %s
+""" % (userdn, userpass, new_password))
+
+        # discard the old creds (i.e. get rid of our valid Kerberos ticket)
+        del test_creds
+        test_creds = self.insta_creds(creds)
+        test_creds.set_password(userpass)
+
+        # for Kerberos, logging in with the old password fails
+        if creds.get_kerberos_state() == MUST_USE_KERBEROS:
+            self.assertLoginFailure(self.host_url, test_creds, self.lp)
+            info_msg = 'Test Kerberos login with old password fails'
+            expectBadPwdTime = ("greater", badPasswordTime)
+            res = self._check_account(userdn,
+                                      badPwdCount=1,
+                                      badPasswordTime=expectBadPwdTime,
+                                      logonCount=logonCount,
+                                      lastLogon=lastLogon,
+                                      lastLogonTimestamp=lastLogonTimestamp,
+                                      userAccountControl=UF_NORMAL_ACCOUNT,
+                                      msDSUserAccountControlComputed=0,
+                                      msg=info_msg)
+            badPasswordTime = int(res[0]["badPasswordTime"][0])
+        else:
+            # for NTLM, logging in with the old password succeeds
+            user_ldb = SamDB(url=self.host_url, credentials=test_creds,
+                             lp=self.lp)
+            info_msg = 'Test NTLM login with old password succeeds'
+            res = self._check_account(userdn,
+                                      badPwdCount=0,
+                                      badPasswordTime=badPasswordTime,
+                                      logonCount=logonCount,
+                                      lastLogon=lastLogon,
+                                      lastLogonTimestamp=lastLogonTimestamp,
+                                      userAccountControl=UF_NORMAL_ACCOUNT,
+                                      msDSUserAccountControlComputed=0,
+                                      msg=info_msg)
+
+        # check logging in with the new password succeeds
+        test_creds.set_password(new_password)
+        user_ldb = SamDB(url=self.host_url, credentials=test_creds, lp=self.lp)
+        res = self._check_account(userdn,
+                                  badPwdCount=0,
+                                  badPasswordTime=badPasswordTime,
+                                  logonCount=(logoncount_relation, logonCount),
+                                  lastLogon=(lastlogon_relation, lastLogon),
+                                  lastLogonTimestamp=lastLogonTimestamp,
+                                  userAccountControl=UF_NORMAL_ACCOUNT,
+                                  msDSUserAccountControlComputed=0,
+                                  msg='Test login with new password succeeds')
+
+    def test_login_basics_krb5(self):
+        self._test_login_basics(self.lockout1krb5_creds)
+
+    def test_login_basics_ntlm(self):
+        self._test_login_basics(self.lockout1ntlm_creds)
+
+host_url = "ldap://%s" % host
+
+TestProgram(module=__name__, opts=subunitopts)
index bca002f0d505ece9810bdf1df4592ebdf7ec1e84..4504a2b09d8189c7e96e02bc65adbef8d5fd80bd 100755 (executable)
@@ -811,6 +811,17 @@ plantestsuite_loadlist("samba4.ldap.sort.python(ad_dc_ntvfs)", "ad_dc_ntvfs", [p
 plantestsuite_loadlist("samba4.ldap.vlv.python(ad_dc_ntvfs)", "ad_dc_ntvfs", [python, os.path.join(samba4srcdir, "dsdb/tests/python/vlv.py"), '$SERVER', '-U"$USERNAME%$PASSWORD"', '--workgroup=$DOMAIN', '$LOADLIST', '$LISTOPT'])
 plantestsuite_loadlist("samba4.ldap.linked_attributes.python(ad_dc_ntvfs)", "ad_dc_ntvfs:local", [python, os.path.join(samba4srcdir, "dsdb/tests/python/linked_attributes.py"), '$PREFIX_ABS/ad_dc_ntvfs/private/sam.ldb', '-U"$USERNAME%$PASSWORD"', '--workgroup=$DOMAIN', '$LOADLIST', '$LISTOPT'])
 
+# These should be the first tests run against testenvs created by backup/restore
+for env in ['restoredc']:
+    # check that a restored DC matches the original DC (backupfromdc)
+    plantestsuite("samba4.blackbox.ldapcmp_restore", env,
+        ["PYTHON=%s" % python,
+         os.path.join(bbdir, "ldapcmp_restoredc.sh"),
+         '$PREFIX_ABS/backupfromdc', '$PREFIX_ABS/%s' % env])
+    # basic test that we can join the testenv DC
+    plantestsuite("samba4.blackbox.join_ldapcmp", env,
+        ["PYTHON=%s" % python, os.path.join(bbdir, "join_ldapcmp.sh")])
+
 plantestsuite_loadlist("samba4.ldap.rodc.python(rodc)", "rodc",
                        [python,
                         os.path.join(samba4srcdir, "dsdb/tests/python/rodc.py"),
@@ -857,6 +868,13 @@ for env in ["ad_dc_ntvfs"]:
                            extra_path=[os.path.join(samba4srcdir, 'dsdb/tests/python')]
                            )
 
+# this is a basic sanity-check of Kerberos/NTLM user login
+for env in ["restoredc"]:
+    plantestsuite_loadlist("samba4.ldap.login_basics.python(%s)" % env, env,
+        [python, os.path.join(samba4srcdir, "dsdb/tests/python/login_basics.py"),
+         "$SERVER", '-U"$USERNAME%$PASSWORD"', "-W$DOMAIN", "--realm=$REALM",
+         '$LOADLIST', '$LISTOPT'])
+
 planpythontestsuite("ad_dc_ntvfs:local", "samba.tests.upgradeprovisionneeddc")
 planpythontestsuite("ad_dc:local", "samba.tests.posixacl", py3_compatible=True)
 planpythontestsuite("ad_dc_no_nss:local", "samba.tests.posixacl", py3_compatible=True)
@@ -887,8 +905,8 @@ t = "rpc.samr.large-dc"
 plansmbtorture4testsuite(t, "vampire_dc", ['$SERVER', '-U$USERNAME%$PASSWORD', '--workgroup=$DOMAIN'], modname=("samba4.%s.one" % t))
 plansmbtorture4testsuite(t, "vampire_dc", ['$SERVER', '-U$USERNAME%$PASSWORD', '--workgroup=$DOMAIN'], modname="samba4.%s.two" % t)
 
-# some RODC testing
-for env in ['rodc']:
+# RPC smoke-tests for testenvs of interest (RODC, etc)
+for env in ['rodc', 'restoredc']:
     plansmbtorture4testsuite('rpc.echo', env, ['ncacn_np:$SERVER', "-k", "yes", '-U$USERNAME%$PASSWORD', '--workgroup=$DOMAIN'], modname="samba4.rpc.echo")
     plansmbtorture4testsuite('rpc.echo', "%s:local" % env, ['ncacn_np:$SERVER', "-k", "yes", '-P', '--workgroup=$DOMAIN'], modname="samba4.rpc.echo")
     plansmbtorture4testsuite('rpc.echo', "%s:local" % env, ['ncacn_np:$SERVER', "-k", "no", '-Utestallowed\ account%$DC_PASSWORD', '--workgroup=$DOMAIN'], modname="samba4.rpc.echo.testallowed")
@@ -1067,7 +1085,8 @@ for env in [
 
 planpythontestsuite("ad_dc_ntvfs:local", "samba.tests.kcc.kcc_utils")
 
-for env in [ "simpleserver", "fileserver", "nt4_dc", "ad_dc", "ad_dc_ntvfs", "ad_member"]:
+for env in [ "simpleserver", "fileserver", "nt4_dc", "ad_dc", "ad_dc_ntvfs",
+             "ad_member", "restoredc" ]:
     planoldpythontestsuite(env, "netlogonsvc",
                            extra_path=[os.path.join(srcdir(), 'python/samba/tests')],
                            name="samba.tests.netlogonsvc.python(%s)" % env)
@@ -1090,7 +1109,8 @@ for env in ['vampire_dc', 'promoted_dc', 'rodc']:
 # TODO: Verifying the databases really should be a part of the
 # environment teardown.
 # check the databases are all OK. PLEASE LEAVE THIS AS THE LAST TEST
-for env in ["ad_dc_ntvfs", "ad_dc", "fl2000dc", "fl2003dc", "fl2008r2dc", 'vampire_dc', 'promoted_dc']:
+for env in ["ad_dc_ntvfs", "ad_dc", "fl2000dc", "fl2003dc", "fl2008r2dc",
+            'vampire_dc', 'promoted_dc', 'backupfromdc', 'restoredc']:
     plantestsuite("samba4.blackbox.dbcheck(%s)" % env, env + ":local" , ["PYTHON=%s" % python, os.path.join(bbdir, "dbcheck.sh"), '$PREFIX/provision', configuration])
 
 # cmocka tests not requiring a specific encironment
diff --git a/testprogs/blackbox/join_ldapcmp.sh b/testprogs/blackbox/join_ldapcmp.sh
new file mode 100755 (executable)
index 0000000..30d3e1e
--- /dev/null
@@ -0,0 +1,41 @@
+#!/bin/sh
+# Does a join against the testenv's DC and then runs ldapcmp on the resulting DB
+
+. `dirname $0`/subunit.sh
+
+TARGET_DIR="$PREFIX_ABS/join_$SERVER"
+
+cleanup_output_dir()
+{
+    if [ -d $TARGET_DIR ]; then
+        rm -fr $TARGET_DIR
+    fi
+}
+
+SAMBA_TOOL="$PYTHON $BINDIR/samba-tool"
+
+join_dc() {
+    JOIN_ARGS="--targetdir=$TARGET_DIR --server=$SERVER -U$USERNAME%$PASSWORD"
+    $SAMBA_TOOL domain join $REALM dc $JOIN_ARGS --option="netbios name = TESTJOINDC"
+}
+
+ldapcmp_result() {
+    DB1_PATH="tdb://$PREFIX_ABS/$SERVER/private/sam.ldb"
+    DB2_PATH="tdb://$TARGET_DIR/private/sam.ldb"
+
+    # interSiteTopologyGenerator gets periodically updated. With the restored
+    # testenvs, it can sometimes point to the old/deleted DC object still
+    $SAMBA_TOOL ldapcmp $DB1_PATH $DB2_PATH --filter=interSiteTopologyGenerator
+}
+
+cleanup_output_dir
+
+# check that we can join this DC
+testit "check_dc_join" join_dc
+
+# check resulting DB matches server DC
+testit "new_db_matches" ldapcmp_result
+
+cleanup_output_dir
+
+exit $failed
diff --git a/testprogs/blackbox/ldapcmp_restoredc.sh b/testprogs/blackbox/ldapcmp_restoredc.sh
new file mode 100755 (executable)
index 0000000..51951ba
--- /dev/null
@@ -0,0 +1,65 @@
+#!/bin/sh
+# Does an ldapcmp between a newly restored testenv and the original testenv it
+# was based on
+
+if [ $# -lt 2 ]; then
+cat <<EOF
+Usage: $0 ORIG_DC_PREFIX RESTORED_DC_PREFIX
+EOF
+exit 1;
+fi
+
+ORIG_DC_PREFIX_ABS="$1"
+RESTORED_DC_PREFIX_ABS="$2"
+shift 2
+
+. `dirname $0`/subunit.sh
+
+basedn() {
+    SAMDB_PATH=$1
+    $BINDIR/ldbsearch -H $SAMDB_PATH --basedn='' -s base defaultNamingContext | grep defaultNamingContext | awk '{print $2}'
+}
+
+ldapcmp_with_orig() {
+
+    DB1_PATH="tdb://$ORIG_DC_PREFIX_ABS/private/sam.ldb"
+    DB2_PATH="tdb://$RESTORED_DC_PREFIX_ABS/private/sam.ldb"
+
+    # check if the 2 DCs are in different domains
+    DC1_BASEDN=$(basedn $DB1_PATH)
+    DC2_BASEDN=$(basedn $DB2_PATH)
+    BASE_DN_OPTS=""
+
+    # if necessary, pass extra args to ldapcmp to handle the difference in base DNs
+    if [ "$DC1_BASEDN" != "$DC2_BASEDN" ] ; then
+        BASE_DN_OPTS="--base=$DC1_BASEDN --base2=$DC2_BASEDN"
+    fi
+
+    # the restored DC will remove DNS entries for the old DC(s)
+    IGNORE_ATTRS="dnsRecord,dNSTombstoned"
+
+    # DC2 joined DC1, so it will have different DRS info
+    IGNORE_ATTRS="$IGNORE_ATTRS,msDS-NC-Replica-Locations,msDS-HasInstantiatedNCs"
+    IGNORE_ATTRS="$IGNORE_ATTRS,interSiteTopologyGenerator"
+
+    # there's a servicePrincipalName that uses the objectGUID of the DC's NTDS
+    # Settings that will differ between the two DCs
+    IGNORE_ATTRS="$IGNORE_ATTRS,servicePrincipalName"
+
+    # the restore changes the new DC's password twice
+    IGNORE_ATTRS="$IGNORE_ATTRS,lastLogonTimestamp"
+
+    # The RID pools get bumped during the restore process
+    IGNORE_ATTRS="$IGNORE_ATTRS,rIDAllocationPool,rIDAvailablePool"
+
+    # these are just differences between provisioning a domain and joining a DC
+    IGNORE_ATTRS="$IGNORE_ATTRS,localPolicyFlags,operatingSystem,displayName"
+
+    LDAPCMP_CMD="$PYTHON $BINDIR/samba-tool ldapcmp"
+    $LDAPCMP_CMD $DB1_PATH $DB2_PATH --two --filter=$IGNORE_ATTRS $BASE_DN_OPTS
+}
+
+# check that the restored testenv DC basically matches the original
+testit "orig_dc_matches" ldapcmp_with_orig
+
+exit $failed