[PATCH: Domain backup samba-tool command]

Aaron Haslett aaronhaslett at catalyst.net.nz
Fri Mar 23 04:59:42 UTC 2018


The exists shell script for backing up a domain doesn't lock things
properly while doing the backup and could end up with a corrupt backup
or cause a lockup.  Here's a new python script that actually works,
along with tests and required fixes.

-------------- next part --------------
From 479ddb468518e7845fc0b9ac97e0d32984d4d606 Mon Sep 17 00:00:00 2001
From: Aaron <aaronhaslett at catalyst.net.nz>
Date: Fri, 23 Mar 2018 15:10:49 +1300
Subject: [PATCH 1/7] netcmd: added domain backup tool and test

Signed-off-by: Aaron Haslett <aaronhaslett at catalyst.net.nz>
---
 python/samba/netcmd/domain.py        |   2 +
 python/samba/netcmd/domain_backup.py | 157 +++++++++++++++++++++++++++++++++++
 python/samba/tests/domain_backup.py  | 141 +++++++++++++++++++++++++++++++
 selftest/knownfail                   |   1 +
 source4/scripting/bin/samba_backup   |  97 ----------------------
 source4/selftest/tests.py            |   3 +
 6 files changed, 304 insertions(+), 97 deletions(-)
 create mode 100644 python/samba/netcmd/domain_backup.py
 create mode 100644 python/samba/tests/domain_backup.py
 delete mode 100755 source4/scripting/bin/samba_backup

diff --git a/python/samba/netcmd/domain.py b/python/samba/netcmd/domain.py
index d3b9b0c..3c90acb 100644
--- a/python/samba/netcmd/domain.py
+++ b/python/samba/netcmd/domain.py
@@ -4325,3 +4325,5 @@ class cmd_domain(SuperCommand):
     subcommands["tombstones"] = cmd_domain_tombstones()
     subcommands["schemaupgrade"] = cmd_domain_schema_upgrade()
     subcommands["functionalprep"] = cmd_domain_functional_prep()
+    from domain_backup import cmd_domain_backup
+    subcommands["backup"] = cmd_domain_backup()
diff --git a/python/samba/netcmd/domain_backup.py b/python/samba/netcmd/domain_backup.py
new file mode 100644
index 0000000..120be20
--- /dev/null
+++ b/python/samba/netcmd/domain_backup.py
@@ -0,0 +1,157 @@
+# domain_backup
+#
+# Copyright Andrew Bartlett <abartlet at samba.org>
+#
+# This program 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; either version 3 of the License, or
+# (at your option) any later version.
+#
+# 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 datetime, os, sys, tarfile, subprocess, logging, shutil, errno
+import samba
+from samba import Ldb
+import tdb
+
+class cmd_domain_backup(samba.netcmd.Command):
+    '''Backup domain directories into a tar with proper locking.'''
+    synopsis = "%prog [options]"
+    takes_optiongroups = {'sambaopts': samba.getopt.SambaOptions}
+
+    takes_options = [
+        samba.netcmd.Option("--skip-encrypted-secrets",
+                            help="Don't backup encrypted_secrets.key",
+                            action="store_true"),
+        samba.netcmd.Option("--output-dir", help="Output directory", type=str),
+    ]
+
+    def run(self, sambaopts=None, skip_encrypted_secrets=False,
+                  output_dir=None):
+        logger = logging.getLogger()
+        logger.setLevel(logging.DEBUG)
+        logger.addHandler(logging.StreamHandler(sys.stdout))
+
+        # Get the absolute paths of all the directories we're going to backup
+        lp = sambaopts.get_loadparm()
+        p = samba.provision.provision_paths_from_lp(lp, lp.get('realm'))
+        assert p.samdb and os.path.exists(p.samdb), 'No sam.db found.  This'+\
+               ' backup tool is only for AD DCs'
+               
+        dbs_with_children = [p.samdb, p.secrets]
+        up_from_private = os.path.abspath(os.path.join(p.private_dir, '..'))
+        backup_dirs = [p.private_dir, p.state_dir, p.sysvol,
+                       os.path.join(up_from_private, 'etc')]
+        logger.info('running backup on dirs: {}'.format(backup_dirs))
+
+        # Recursively get all file paths in the backup directories
+        all_files = [os.path.join(wd,fn) for bd in backup_dirs
+                     for (wd,_,fns) in os.walk(bd) for fn in fns]
+        if skip_encrypted_secrets:
+            es_fn = os.path.join(p.private_dir, 'encrypted_secrets.key')
+            logger.info('skipping ' + es_fn)
+            all_files.remove(es_fn)
+
+        # Remove any backup files
+        backup_ext = '.bak'
+        bak_files = [f for f in all_files if f.endswith(backup_ext)]
+        for f in bak_files:
+            all_files.remove(f)
+            os.remove(f)
+
+        # Construct our routine for running the tdbbackup tool.
+        # Find the binary first:
+        tdbbackup_paths = [d for d in [samba.param.bin_dir()] +\
+                                      os.getenv('PATH').split(os.pathsep)
+                           if os.path.exists(os.path.join(d,'tdbbackup'))]
+        assert tdbbackup_paths, 'tdbbackup tool not found'
+        tdbbackup_cmd = [os.path.join(tdbbackup_paths[0], 'tdbbackup'),
+                         '-r', '-s', backup_ext]#-r means read locks.
+        def run_tdbbackup(path):
+            status = subprocess.call(tdbbackup_cmd + [path],
+                                     close_fds=True, shell=False)
+            # If the call failed, bail out unless the errno was EINVAL
+            if status != 0:
+                try:
+                    tdb.open(path)
+                except Exception as e:
+                    if e.errno != errno.EINVAL:
+                        raise e
+                    return
+            assert os.path.exists(path + backup_ext), 'tdbbackup said it'+\
+                                '  succeeded but it didn\'t '+\
+                                'make a {} file for {}'.format(backup_ext, path)
+                   
+
+        # Start the backup.  First, handle ldb files that have other ldb/tdb 
+        # files linked to them.Start a transaction on the ldb file, then 
+        # use tdbbackup to back up the DB itself and all linked DBs.  If there
+        # are related files that are not DBs, just copy them.
+        private_files = [f for f in all_files if f.startswith(p.private_dir)]
+        for ldb_path in dbs_with_children:
+            ldb_obj = Ldb(ldb_path, lp=lp)
+            logger.info('Starting transaction on ' + ldb_path)
+            ldb_obj.transaction_start()
+
+            db_name = os.path.splitext(os.path.basename(ldb_path))[0]
+            for path in private_files:
+                # All files that start with the same name as a specific LDB 
+                # are downstream of it, by convention.
+                if path[len(p.private_dir)+1:].startswith(db_name):
+                    if path.endswith('tdb') or path.endswith('ldb'):
+                        logger.info('   tdbbackup: locked/related file ' + path)
+                        run_tdbbackup(path)
+                    else:
+                        logger.info('   copy: locked/related file ' + path)
+                        shutil.copyfile(path, path + backup_ext)
+            ldb_obj.transaction_cancel()
+
+        # Now handle all the LDB and TDB files that are not linked to 
+        # anything else.  Use transactions for LDBs.
+        for path in all_files:
+            if not os.path.exists(path + backup_ext):
+                if path.endswith('.ldb'):
+                    logger.info('Starting transaction on solo db: ' + path)
+                    ldb_obj = Ldb(path, lp=lp)
+                    ldb_obj.transaction_start()
+                    logger.info('   running tdbbackup on the same file')
+                    run_tdbbackup(path)
+                    ldb_obj.transaction_cancel()
+                elif path.endswith('.tdb'):
+                    logger.info('running tdbbackup on lone tdb file ' + path)
+                    run_tdbbackup(path)
+
+        # Now make the backup tar file and add all 
+        # backed up files and any other files to it.
+        output_dir = output_dir or os.environ['SELFTEST_PREFIX']
+        temp_tar_name = os.path.join(output_dir, 'INCOMPLETEsambabackupfile')
+        tar = tarfile.open(temp_tar_name, 'w:bz2')
+
+        backup_root = os.path.commonprefix(backup_dirs)
+        logger.info('building backup tar with root ' + backup_root)
+        for path in all_files:
+            arc_path = os.path.relpath(path, backup_root)
+            if os.path.exists(path + backup_ext):
+                logger.info('   adding backup ' + arc_path + backup_ext +\
+                            ' to tar and deleting file')
+                tar.add(path + backup_ext, arcname=arc_path)
+                os.remove(path + backup_ext)
+            elif path.endswith('.ldb') or path.endswith('.tdb'):
+                logger.info('   skipping ' + arc_path)
+            else:
+                logger.info('   adding misc file ' + arc_path)
+                tar.add(path, arcname=arc_path)
+
+        tar.close()
+        time_str = datetime.datetime.now().isoformat().replace(':','-')
+        os.rename(temp_tar_name, os.path.join(output_dir,
+                    'samba-backup-{}.tar.bz2'.format(time_str)))
+
+        logger.info('Backup succeeded... Probably.')
diff --git a/python/samba/tests/domain_backup.py b/python/samba/tests/domain_backup.py
new file mode 100644
index 0000000..7f793ee
--- /dev/null
+++ b/python/samba/tests/domain_backup.py
@@ -0,0 +1,141 @@
+# Unix SMB/CIFS implementation.
+# Copyright (C) Andrew Bartlett <abartlet at samba.org>
+#
+# This program 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; either version 3 of the License, or
+# (at your option) any later version.
+#
+# 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 samba.credentials import Credentials
+from samba import gensec, auth, tests, provision, param
+import tarfile, os
+from samba.tests.samba_tool.base import SambaToolCmdTest
+
+# Test the domain backup tool by setting up a client with the testenv config,
+# then setting up a server on a restored backup. The test passes or fails
+# depending on whether the session keys are the same on the client and server.
+class DomainBackup(SambaToolCmdTest):
+
+    def test_domain_backup_success(self):
+        self.nottest_domain_backup(kill_password=False)
+
+    def test_domain_backup_fail(self):
+        self.nottest_domain_backup(kill_password=True)
+
+    def nottest_domain_backup(self, kill_password=False):
+        client_finished = False
+        server_finished = False
+        server_to_client = b""
+        client_to_server = b""
+
+        # Make a clean temp directory for the backup
+        tmp_backup_dir = os.path.join(os.environ["SELFTEST_PREFIX"],
+                                      ".backup-test-tmp")
+        if os.path.exists(tmp_backup_dir):
+            import shutil
+            shutil.rmtree(tmp_backup_dir)
+        os.mkdir(tmp_backup_dir)
+
+        # Initialise client and run an update
+        client_lp = tests.env_loadparm()
+        client_settings = {"lp_ctx": client_lp,
+                           "target_hostname": client_lp.get("netbios name")}
+        client_lp.set("spnego:simulate_w2k", "no")
+        gensec_client = gensec.Security.start_client(client_settings)
+        gensec_client.set_credentials(self.get_credentials())
+        gensec_client.want_feature(gensec.FEATURE_SEAL)
+        gensec_client.start_mech_by_sasl_name("GSSAPI")
+        result = gensec_client.update(server_to_client)
+        (client_finished, client_to_server) = result
+
+        # Get the root of the backup folders from loadparm, we'll use this later
+        p = provision.provision_paths_from_lp(client_lp, client_lp.get("realm"))
+        backup_dirs = [p.private_dir, p.state_dir, p.sysvol]
+        backup_root = os.path.commonprefix(backup_dirs)
+
+        # Run the backup and check we got one backup tar file
+        (result, out, err) = self.runsubcmd("domain", "backup",
+                                            "--output-dir", tmp_backup_dir)
+        self.assertCmdSuccess(result, out, err,
+                              "Ensuring domain backup command ran successfully")
+        tar_files = [fn for fn in os.listdir(tmp_backup_dir)
+                     if fn.startswith("samba-backup-") and
+                        fn.endswith(".tar.bz2")]
+        assert len(tar_files) == 1, "expected domain backup to create one" +\
+                                    "tar file but got {}".format(len(tar_files))
+
+        # Fill the temp backup dir with the empty folder that are needed,
+        # then extract the backup
+        for tmp_subdir_to_make in ["lockdir", "cachedir", "pid"]:
+            os.mkdir(os.path.join(tmp_backup_dir, tmp_subdir_to_make))
+        tf = tarfile.open(os.path.join(tmp_backup_dir, tar_files[0]))
+        tf.extractall(tmp_backup_dir)
+
+        # Make paths in the restored config file point the new temp directory
+        cfg_fn = os.path.join(os.getcwd(), tmp_backup_dir, "etc", "smb.conf")
+        sed_lines = ""
+        with open(cfg_fn, "r") as cfg_file:
+            sed_lines = cfg_file.read().replace(backup_root,
+                                 os.path.realpath(tmp_backup_dir) + "/")
+        with open(cfg_fn, "w") as cfg_file:
+            cfg_file.write(sed_lines)
+
+        # If this is a test that should fail, we"ll kill the password
+        if kill_password:
+            chgtdcpass_cmd = "./source4/scripting/devel/chgtdcpass -s " + cfg_fn
+            self.check_output(chgtdcpass_cmd)
+            self.check_output(chgtdcpass_cmd)
+
+        # Initialise server to the restored domain
+        server_lp = param.LoadParm()
+        server_settings = {"target_hostname": server_lp.get("netbios name"),
+                           "lp_ctx": server_lp}
+        server_lp.set("spnego:simulate_w2k", "no")
+        gensec_server = gensec.Security.start_server(settings=server_settings,
+                                auth_context=auth.AuthContext(lp_ctx=server_lp))
+        creds = Credentials()
+        creds.guess(server_lp)
+        creds.set_machine_account(server_lp)
+        gensec_server.set_credentials(creds)
+        gensec_server.want_feature(gensec.FEATURE_SEAL)
+        gensec_server.start_mech_by_sasl_name("GSSAPI")
+
+        exception = None
+        try:
+            # Run update routines until done
+            while True:
+                if not server_finished:
+                    print("running server gensec_update")
+                    result = gensec_server.update(client_to_server)
+                    (server_finished, server_to_client) = result
+
+                if not client_finished:
+                    print("running client gensec_update")
+                    result = gensec_client.update(server_to_client)
+                    (client_finished, client_to_server) = result
+
+                if client_finished and server_finished:
+                    break
+        except Exception as e:
+            exception = e
+
+        if kill_password:
+            # If the test was intended to fail, make sure
+            self.assertIsNotNone(exception)
+        else:
+            # If the test was intended to pass, raise any exceptions or 
+            # do a final session key equality check.
+            if exception:
+                raise exception
+            client_session_key = gensec_client.session_key()
+            server_session_key = gensec_server.session_key()
+            self.assertEqual(client_session_key, server_session_key)
diff --git a/selftest/knownfail b/selftest/knownfail
index a2aeed2..54a43b7 100644
--- a/selftest/knownfail
+++ b/selftest/knownfail
@@ -343,3 +343,4 @@
 # Disabling NTLM means you can't use samr to change the password
 ^samba.tests.ntlmdisabled.python\(ktest\).ntlmdisabled.NtlmDisabledTests.test_samr_change_password\(ktest\)
 ^samba.tests.ntlmdisabled.python\(ad_dc_no_ntlm\).ntlmdisabled.NtlmDisabledTests.test_ntlm_connection\(ad_dc_no_ntlm\)
+.*samba.tests.domain_backup.DomainBackup.test_domain_backup_fail
diff --git a/source4/scripting/bin/samba_backup b/source4/scripting/bin/samba_backup
deleted file mode 100755
index 3e22abe..0000000
--- a/source4/scripting/bin/samba_backup
+++ /dev/null
@@ -1,97 +0,0 @@
-#!/bin/sh
-#
-# Copyright (C) Matthieu Patou <mat at matws.net> 2010-2011
-#
-# This program 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; either version 3 of the License, or
-# (at your option) any later version.
-#
-# 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/>.
-#
-# Revised 2013-09-25, Brian Martin, as follows:
-#    - Allow retention period ("DAYS") to be specified as a parameter.
-#    - Allow individual positional parameters to be left at the default
-#      by specifying "-"
-#    - Use IS0 8601 standard dates (yyyy-mm-dd instead of mmddyyyy).
-#    - Display tar exit codes when reporting errors.
-#    - Don't send error messages to /dev/null, so we know what failed.
-#    - Suppress useless tar "socket ignored" message.
-#    - Fix retention period bug when deleting old backups ($DAYS variable
-#      could be set, but was ignored).
-
-
-
-FROMWHERE=/usr/local/samba
-WHERE=/usr/local/backups
-DAYS=90				# Set default retention period.
-if [ -n "$1" ] && [ "$1" = "-h" -o "$1" = "--usage" ]; then
-	echo "samba_backup [provisiondir] [destinationdir] [retpd]"
-	echo "Will backup your provision located in provisiondir to archive stored"
-	echo "in destinationdir for retpd days. Use - to leave an option unchanged."
-	echo "Default provisiondir: $FROMWHERE"
-	echo "Default destinationdir: $WHERE"
-	echo "Default destinationdir: $DAYS"
-	exit 0
-fi
-
-[ -n "$1" -a "$1" != "-" ]&&FROMWHERE=$1	# Use parm or default if "-".  Validate later.
-[ -n "$2" -a "$2" != "-" ]&&WHERE=$2		# Use parm or default if "-".  Validate later.
-[ -n "$3" -a "$3" -eq "$3" 2> /dev/null ]&&DAYS=$3	# Use parm or default if non-numeric (incl "-").
-
-DIRS="private etc sysvol"
-#Number of days to keep the backup
-WHEN=`date +%Y-%m-%d`	# ISO 8601 standard date.
-
-if [ ! -d $WHERE ]; then
-	echo "Missing backup directory $WHERE"
-	exit 1
-fi
-
-if [ ! -d $FROMWHERE ]; then
-	echo "Missing or wrong provision directory $FROMWHERE"
-	exit 1
-fi
-
-cd $FROMWHERE
-for d in $DIRS;do
-	relativedirname=`find . -type d -name "$d" -prune`
-	n=`echo $d | sed 's/\//_/g'`
-	if [ "$d" = "private" ]; then
-		find $relativedirname -name "*.ldb.bak" -exec rm {} \;
-		for ldb in `find $relativedirname -name "*.ldb"`; do
-			tdbbackup $ldb
-			Status=$?	# Preserve $? for message, since [ alters it.
-			if [ $Status -ne 0 ]; then
-				echo "Error while backing up $ldb - status $Status"
-				exit 1
-			fi
-		done
-		# Run the backup.
-		#    --warning=no-file-ignored set to suppress "socket ignored" messages.
-		tar cjf ${WHERE}/samba4_${n}.${WHEN}.tar.bz2  $relativedirname --exclude=\*.ldb --warning=no-file-ignored --transform 's/.ldb.bak$/.ldb/'
-		Status=$?	# Preserve $? for message, since [ alters it.
-		if [ $Status -ne 0 -a $Status -ne 1 ]; then	# Ignore 1 - private dir is always changing.
-			echo "Error while archiving ${WHERE}/samba4_${n}.${WHEN}.tar.bz2 - status = $Status"
-			exit 1
-		fi
-		find $relativedirname -name "*.ldb.bak" -exec rm {} \;
-	else
-		# Run the backup.
-		#    --warning=no-file-ignored set to suppress "socket ignored" messages.
-		tar cjf ${WHERE}/${n}.${WHEN}.tar.bz2  $relativedirname --warning=no-file-ignored
-		Status=$?	# Preserve $? for message, since [ alters it.
-		if [ $Status -ne 0 ]; then
-			echo "Error while archiving ${WHERE}/${n}.${WHEN}.tar.bz2 - status = $Status"
-			exit 1
-		fi
-	fi
-done
-
-find $WHERE -name "samba4_*bz2" -mtime +$DAYS -exec rm  {} \;
diff --git a/source4/selftest/tests.py b/source4/selftest/tests.py
index ef752a5..757ce92 100755
--- a/source4/selftest/tests.py
+++ b/source4/selftest/tests.py
@@ -683,6 +683,9 @@ planoldpythontestsuite("fl2003dc:local",
 planoldpythontestsuite("ad_dc",
                        "samba.tests.password_hash_ldap",
                        extra_args=['-U"$USERNAME%$PASSWORD"'])
+planoldpythontestsuite("ad_dc:local",
+                       "samba.tests.domain_backup",
+                       extra_args=['-U"$USERNAME%$PASSWORD"'])
 # Encrypted secrets
 # ensure default provision (ad_dc) and join (vampire_dc)
 # encrypt secret values on disk.
-- 
2.7.4


From b3fa317ef0f3c164da3496ead3767a370b841917 Mon Sep 17 00:00:00 2001
From: Aaron <aaronhaslett at catalyst.net.nz>
Date: Fri, 23 Mar 2018 15:18:17 +1300
Subject: [PATCH 2/7] auth: keytab invalidation test

Signed-off-by: Aaron Haslett <aaronhaslett at catalyst.net.nz>
---
 selftest/knownfail            |   1 +
 selftest/tests.py             |   2 +
 source4/auth/tests/kerberos.c | 104 ++++++++++++++++++++++++++++++++++++++++++
 source4/auth/wscript_build    |   6 +++
 4 files changed, 113 insertions(+)
 create mode 100644 source4/auth/tests/kerberos.c

diff --git a/selftest/knownfail b/selftest/knownfail
index 54a43b7..c9266ab 100644
--- a/selftest/knownfail
+++ b/selftest/knownfail
@@ -343,4 +343,5 @@
 # Disabling NTLM means you can't use samr to change the password
 ^samba.tests.ntlmdisabled.python\(ktest\).ntlmdisabled.NtlmDisabledTests.test_samr_change_password\(ktest\)
 ^samba.tests.ntlmdisabled.python\(ad_dc_no_ntlm\).ntlmdisabled.NtlmDisabledTests.test_ntlm_connection\(ad_dc_no_ntlm\)
+^samba.unittests.kerberos.test_krb5_remove_obsolete_keytab_entries_many
 .*samba.tests.domain_backup.DomainBackup.test_domain_backup_fail
diff --git a/selftest/tests.py b/selftest/tests.py
index e69bc31..c209033 100644
--- a/selftest/tests.py
+++ b/selftest/tests.py
@@ -184,5 +184,7 @@ plantestsuite("samba.unittests.tldap", "none",
               [os.path.join(bindir(), "default/source3/test_tldap")])
 plantestsuite("samba.unittests.rfc1738", "none",
               [os.path.join(bindir(), "default/lib/util/test_rfc1738")])
+plantestsuite("samba.unittests.kerberos", "none",
+              [os.path.join(bindir(), "test_kerberos")])
 plantestsuite("samba.unittests.ms_fnmatch", "none",
               [os.path.join(bindir(), "default/lib/util/test_ms_fnmatch")])
diff --git a/source4/auth/tests/kerberos.c b/source4/auth/tests/kerberos.c
new file mode 100644
index 0000000..7a4441f
--- /dev/null
+++ b/source4/auth/tests/kerberos.c
@@ -0,0 +1,104 @@
+#include <time.h>
+#include <stdlib.h>
+#include <stdarg.h>
+#include <stddef.h>
+#include <setjmp.h>
+#include <stdint.h>
+#include <cmocka.h>
+
+#include "includes.h"
+#include "system/kerberos.h"
+#include "auth/kerberos/kerberos.h"
+#include "auth/credentials/credentials.h"
+#include "auth/credentials/credentials_proto.h"
+#include "auth/credentials/credentials_krb5.h"
+#include "auth/kerberos/kerberos_credentials.h"
+#include "auth/kerberos/kerberos_util.h"
+
+static void internal_obsolete_keytab_test(int num_principals, int num_kvnos,
+                                          krb5_kvno kvno, const char *kt_name)
+{
+	krb5_context krb5_ctx;
+	krb5_keytab keytab;
+	krb5_keytab_entry kt_entry;
+	krb5_kt_cursor cursor;
+	krb5_error_code code;
+
+        int i,j;
+        char princ_name[6] = "user0";
+        bool found_previous;
+        const char *error_str;
+
+	TALLOC_CTX *tmp_ctx = talloc_new(NULL);
+	krb5_principal *principals = talloc_zero_array(tmp_ctx,
+                                                       krb5_principal,
+                                                       num_principals);
+        krb5_init_context(&krb5_ctx);
+        krb5_kt_resolve(krb5_ctx, kt_name, &keytab);
+        ZERO_STRUCT(kt_entry);
+
+        for(i=0; i<num_principals; i++){
+                princ_name[4] = (char)i+48;
+                krb5_make_principal(krb5_ctx, &(principals[i]),
+                                    "samba.example.com", princ_name, NULL);
+                kt_entry.principal = principals[i];
+                for(j=0; j<num_kvnos; j++){
+                        kt_entry.vno = j+1;
+                        krb5_kt_add_entry(krb5_ctx, keytab, &kt_entry);
+                }
+        }
+
+	code = krb5_kt_start_seq_get(krb5_ctx, keytab, &cursor);
+        assert_int_equal(code, 0);
+        for(i=0; i<num_principals; i++){
+                princ_name[4] = (char)i+48;
+                for(j=0; j<num_kvnos; j++){
+                        code = krb5_kt_next_entry(krb5_ctx, keytab,
+                                                  &kt_entry, &cursor);
+                        assert_int_equal(code, 0);
+                        assert_string_equal(princ_name,
+                                   *(kt_entry.principal->name.name_string.val));
+                        assert_int_equal(kt_entry.vno, j+1);
+                }
+        }
+
+        smb_krb5_remove_obsolete_keytab_entries(tmp_ctx, krb5_ctx, keytab,
+                                                num_principals, principals,
+                                                kvno, &found_previous,
+                                                &error_str);
+
+	code = krb5_kt_start_seq_get(krb5_ctx, keytab, &cursor);
+        assert_int_equal(code, 0);
+        for(i=0; i<num_principals; i++){
+                princ_name[4] = (char)i+48;
+                code = krb5_kt_next_entry(krb5_ctx, keytab, &kt_entry, &cursor);
+                assert_int_equal(code, 0);
+                assert_string_equal(princ_name,
+                        *(kt_entry.principal->name.name_string.val));
+                assert_int_equal(kt_entry.vno, kvno-1);
+        }
+        code = krb5_kt_next_entry(krb5_ctx, keytab, &kt_entry, &cursor);
+        assert_int_not_equal(code, 0);
+}
+
+static void test_krb5_remove_obsolete_keytab_entries_many(void **state)
+{
+        internal_obsolete_keytab_test(5, 4, (krb5_kvno)5, "MEMORY:LOL2");
+}
+
+static void test_krb5_remove_obsolete_keytab_entries_one(void **state)
+{
+        internal_obsolete_keytab_test(1, 2, (krb5_kvno)3, "MEMORY:LOL");
+}
+
+int main(int argc, const char **argv)
+{
+	const struct CMUnitTest tests[] = {
+		cmocka_unit_test(test_krb5_remove_obsolete_keytab_entries_one),
+		cmocka_unit_test(test_krb5_remove_obsolete_keytab_entries_many),
+	};
+
+        samba_start_debugger();
+	cmocka_set_message_output(CM_OUTPUT_SUBUNIT);
+	return cmocka_run_group_tests(tests, NULL, NULL);
+}
diff --git a/source4/auth/wscript_build b/source4/auth/wscript_build
index 5786fa5..a982476 100644
--- a/source4/auth/wscript_build
+++ b/source4/auth/wscript_build
@@ -42,6 +42,12 @@ bld.SAMBA_SUBSYSTEM('auth4_sam',
 	deps=''
 	)
 
+bld.SAMBA_BINARY('test_kerberos',
+        source='tests/kerberos.c',
+        deps='cmocka authkrb5 krb5samba com_err CREDENTIALS_KRB5',
+        local_include=False,
+        install=False
+        )
 
 for env in bld.gen_python_environments():
 	pytalloc_util = bld.pyembed_libname('pytalloc-util')
-- 
2.7.4


From 43ae94946a794597c8ea46bb2cc10525b4b6b33b Mon Sep 17 00:00:00 2001
From: Aaron <aaronhaslett at catalyst.net.nz>
Date: Fri, 23 Mar 2018 15:19:20 +1300
Subject: [PATCH 3/7] netcmd: domain backup offline test with ldapcmp

Signed-off-by: Aaron Haslett <aaronhaslett at catalyst.net.nz>
---
 python/samba/tests/domain_backup_offline.py | 71 +++++++++++++++++++++++++++++
 source4/selftest/tests.py                   |  2 +
 2 files changed, 73 insertions(+)
 create mode 100644 python/samba/tests/domain_backup_offline.py

diff --git a/python/samba/tests/domain_backup_offline.py b/python/samba/tests/domain_backup_offline.py
new file mode 100644
index 0000000..0eb3ba3
--- /dev/null
+++ b/python/samba/tests/domain_backup_offline.py
@@ -0,0 +1,71 @@
+# Unix SMB/CIFS implementation.
+# Copyright (C) Aaron Haslett<aaronhaslett at catalyst.net.nz> 2018
+#
+# This program 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; either version 3 of the License, or
+# (at your option) any later version.
+#
+# 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 tarfile, os
+from samba.tests.samba_tool.base import SambaToolCmdTest
+
+class DomainBackupOffline(SambaToolCmdTest):
+
+    # Test the "samba-tool domain backup" command with ldapcmp
+    def test_domain_backup_offline(self):
+        # First, make sure we have a clean ./st/.backup-test-tmp directory
+        tmp_backup_dir = os.path.join("st", ".backup-test-tmp")
+        if os.path.exists(tmp_backup_dir):
+            import shutil
+            shutil.rmtree(tmp_backup_dir)
+        if not os.path.exists("st"):
+            os.mkdir("st")
+        os.mkdir(tmp_backup_dir)
+        prov_dir = os.path.join(tmp_backup_dir, "offlinebackuptest")
+        os.mkdir(prov_dir)
+
+        # Provision domain
+        (result, out, err) = self.runsubcmd("domain", "provision",
+                                            "--domain", "FOO",
+                                            "--realm", "foo.example.com",
+                                            "--targetdir", prov_dir,
+                                            "--use-ntvfs")
+        self.assertCmdSuccess(result, out, err,
+                       "Ensuring domain provision command ran successfully")
+
+        # Run the backup and check we got one backup tar file
+        (result, out, err) = self.runsubcmd("domain", "backup",
+                                            "--output-dir", tmp_backup_dir,
+                                            "-s", os.path.join(prov_dir, 'etc',
+                                                               'smb.conf'))
+        self.assertCmdSuccess(result, out, err,
+                        "Ensuring domain backup command ran successfully")
+        tar_files = [fn for fn in os.listdir(tmp_backup_dir)
+                     if fn.startswith("samba-backup-")
+                        and fn.endswith(".tar.bz2")]
+        assert len(tar_files) == 1, "expected domain backup to create one tar"+\
+                                    " file but got {}".format(len(tar_files))
+
+        # Open the backup and restore it to a new directory
+        tf = tarfile.open(os.path.join(tmp_backup_dir, tar_files[0]))
+        bak_dir = os.path.join(tmp_backup_dir, "extracted_backup")
+        tf.extractall(bak_dir)
+        sam_fn = os.path.join("private", "sam.ldb")
+
+        # Compare the restored sam.ldb with the old one
+        for partition in ["domain", "configuration", "schema",
+                          "dnsdomain", "dnsforest"]:
+            url1 = "tdb://" + os.path.join(os.path.realpath(prov_dir), sam_fn)
+            url2 = "tdb://" + os.path.join(os.path.realpath(bak_dir), sam_fn)
+            (result, out, err) = self.runcmd("ldapcmp", url1, url2, partition)
+            self.assertCmdSuccess(result, out, err,
+                                  "Ensuring ldapcmp command ran successfully")
diff --git a/source4/selftest/tests.py b/source4/selftest/tests.py
index 757ce92..134c84c 100755
--- a/source4/selftest/tests.py
+++ b/source4/selftest/tests.py
@@ -686,6 +686,8 @@ planoldpythontestsuite("ad_dc",
 planoldpythontestsuite("ad_dc:local",
                        "samba.tests.domain_backup",
                        extra_args=['-U"$USERNAME%$PASSWORD"'])
+planoldpythontestsuite("none",
+                       "samba.tests.domain_backup_offline")
 # Encrypted secrets
 # ensure default provision (ad_dc) and join (vampire_dc)
 # encrypt secret values on disk.
-- 
2.7.4


From f3646dad285e559c70d940517c5939439dae813b Mon Sep 17 00:00:00 2001
From: Aaron <aaronhaslett at catalyst.net.nz>
Date: Fri, 23 Mar 2018 15:20:29 +1300
Subject: [PATCH 4/7] param: adding non-global smb.cfg option

Signed-off-by: Aaron Haslett <aaronhaslett at catalyst.net.nz>
---
 lib/param/loadparm.c                | 15 +++++++++++++--
 python/samba/tests/domain_backup.py |  2 +-
 source4/param/pyparam.c             | 19 ++++++++++++++++++-
 3 files changed, 32 insertions(+), 4 deletions(-)

diff --git a/lib/param/loadparm.c b/lib/param/loadparm.c
index b46700d..c771015 100644
--- a/lib/param/loadparm.c
+++ b/lib/param/loadparm.c
@@ -3145,7 +3145,8 @@ bool lpcfg_load_default(struct loadparm_context *lp_ctx)
  *
  * Return True on success, False on failure.
  */
-bool lpcfg_load(struct loadparm_context *lp_ctx, const char *filename)
+static bool lpcfg_load_internal(struct loadparm_context *lp_ctx,
+                                const char *filename, bool set_global)
 {
 	char *n2;
 	bool bRetval;
@@ -3180,7 +3181,7 @@ bool lpcfg_load(struct loadparm_context *lp_ctx, const char *filename)
 	   for a missing smb.conf */
 	reload_charcnv(lp_ctx);
 
-	if (bRetval == true) {
+	if (bRetval == true && set_global) {
 		/* set this up so that any child python tasks will
 		   find the right smb.conf */
 		setenv("SMB_CONF_PATH", filename, 1);
@@ -3194,6 +3195,16 @@ bool lpcfg_load(struct loadparm_context *lp_ctx, const char *filename)
 	return bRetval;
 }
 
+bool lpcfg_load_no_global(struct loadparm_context *lp_ctx, const char *filename)
+{
+    return lpcfg_load_internal(lp_ctx, filename, false);
+}
+
+bool lpcfg_load(struct loadparm_context *lp_ctx, const char *filename)
+{
+    return lpcfg_load_internal(lp_ctx, filename, true);
+}
+
 /**
  * Return the max number of services.
  */
diff --git a/python/samba/tests/domain_backup.py b/python/samba/tests/domain_backup.py
index 7f793ee..e92b470 100644
--- a/python/samba/tests/domain_backup.py
+++ b/python/samba/tests/domain_backup.py
@@ -96,7 +96,7 @@ class DomainBackup(SambaToolCmdTest):
             self.check_output(chgtdcpass_cmd)
 
         # Initialise server to the restored domain
-        server_lp = param.LoadParm()
+        server_lp = param.LoadParm(_filename_for_non_global_lp = cfg_fn)
         server_settings = {"target_hostname": server_lp.get("netbios name"),
                            "lp_ctx": server_lp}
         server_lp.set("spnego:simulate_w2k", "no")
diff --git a/source4/param/pyparam.c b/source4/param/pyparam.c
index f16c2c0..5807bc7 100644
--- a/source4/param/pyparam.c
+++ b/source4/param/pyparam.c
@@ -420,7 +420,24 @@ static PyGetSetDef py_lp_ctx_getset[] = {
 
 static PyObject *py_lp_ctx_new(PyTypeObject *type, PyObject *args, PyObject *kwargs)
 {
-	return pytalloc_reference(type, loadparm_init_global(false));
+	const char *kwnames[] = {"_filename_for_non_global_lp", NULL};
+	PyObject *lp_ctx;
+	const char *conf_filename = NULL;
+	struct loadparm_context *ctx;
+
+	if (!PyArg_ParseTupleAndKeywords(args, kwargs, "|s",
+            discard_const_p(char *, kwnames), &conf_filename)){
+		return NULL;
+	}
+	if (conf_filename != NULL){
+		ctx = loadparm_init(NULL);
+		lp_ctx = pytalloc_reference(type, ctx);
+		lpcfg_load_no_global(PyLoadparmContext_AsLoadparmContext(lp_ctx),
+                                     conf_filename);
+		return lp_ctx;
+	} else{
+		return pytalloc_reference(type, loadparm_init_global(false));
+	}
 }
 
 static Py_ssize_t py_lp_ctx_len(PyObject *self)
-- 
2.7.4


From 395391588e8ae81673ba643d99faeae2eb22e7e1 Mon Sep 17 00:00:00 2001
From: Aaron <aaronhaslett at catalyst.net.nz>
Date: Fri, 23 Mar 2018 15:21:02 +1300
Subject: [PATCH 5/7] ldb: removing prior secret from logs

Bug: https://bugzilla.samba.org/show_bug.cgi?id=13353
Signed-off-by: Aaron Haslett <aaronhaslett at catalyst.net.nz>
---
 lib/ldb-samba/ldif_handlers.c | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/lib/ldb-samba/ldif_handlers.c b/lib/ldb-samba/ldif_handlers.c
index 591bd1e..ecc02e5 100644
--- a/lib/ldb-samba/ldif_handlers.c
+++ b/lib/ldb-samba/ldif_handlers.c
@@ -1706,7 +1706,8 @@ const struct ldb_schema_syntax *ldb_samba_syntax_by_lDAPDisplayName(struct ldb_c
 	return s;
 }
 
-static const char *secret_attributes[] = {DSDB_SECRET_ATTRIBUTES, "secret", NULL};
+static const char *secret_attributes[] = {DSDB_SECRET_ATTRIBUTES, "secret",
+                                          "priorSecret", NULL};
 
 /*
   register the samba ldif handlers
-- 
2.7.4


From ce48e15c97bb8f29a3796a216281f852158c84be Mon Sep 17 00:00:00 2001
From: Aaron <aaronhaslett at catalyst.net.nz>
Date: Fri, 23 Mar 2018 15:22:05 +1300
Subject: [PATCH 6/7] tdb: adding readonly locks mode to tdbbackup tool

Signed-off-by: Aaron Haslett <aaronhaslett at catalyst.net.nz>
---
 lib/tdb/tools/tdbbackup.c | 31 ++++++++++++++++++++++++-------
 1 file changed, 24 insertions(+), 7 deletions(-)

diff --git a/lib/tdb/tools/tdbbackup.c b/lib/tdb/tools/tdbbackup.c
index eb33e25..2689b6f 100644
--- a/lib/tdb/tools/tdbbackup.c
+++ b/lib/tdb/tools/tdbbackup.c
@@ -105,7 +105,7 @@ static int test_fn(TDB_CONTEXT *tdb, TDB_DATA key, TDB_DATA dbuf, void *state)
   this function is also used for restore
 */
 static int backup_tdb(const char *old_name, const char *new_name,
-		      int hash_size, int nolock)
+		      int hash_size, int nolock, int readonly)
 {
 	TDB_CONTEXT *tdb;
 	TDB_CONTEXT *tdb_new;
@@ -145,8 +145,17 @@ static int backup_tdb(const char *old_name, const char *new_name,
 		return 1;
 	}
 
-	if (tdb_transaction_start(tdb) != 0) {
-		printf("Failed to start transaction on old tdb\n");
+	if (readonly){
+		if(tdb_lockall_read(tdb) != 0){
+			printf("Failed to readonly lock on old tdb\n");
+			tdb_close(tdb);
+			tdb_close(tdb_new);
+			unlink(tmp_name);
+			free(tmp_name);
+			return 1;
+		}
+	}else if(tdb_transaction_start(tdb) != 0){
+		printf("Failed to start transaction on db\n");
 		tdb_close(tdb);
 		tdb_close(tdb_new);
 		unlink(tmp_name);
@@ -167,7 +176,11 @@ static int backup_tdb(const char *old_name, const char *new_name,
 	failed = 0;
 
 	/* traverse and copy */
-	count1 = tdb_traverse(tdb, copy_fn, (void *)tdb_new);
+        if(readonly){
+                count1 = tdb_traverse_read(tdb, copy_fn, (void *)tdb_new);
+        }else{
+	        count1 = tdb_traverse(tdb, copy_fn, (void *)tdb_new);
+        }
 	if (count1 < 0 || failed) {
 		fprintf(stderr,"failed to copy %s\n", old_name);
 		tdb_close(tdb);
@@ -251,7 +264,7 @@ static int verify_tdb(const char *fname, const char *bak_name)
 	/* count is < 0 means an error */
 	if (count < 0) {
 		printf("restoring %s\n", fname);
-		return backup_tdb(bak_name, fname, 0, 0);
+		return backup_tdb(bak_name, fname, 0, 0, 0);
 	}
 
 	printf("%s : %d records\n", fname, count);
@@ -282,6 +295,7 @@ static void usage(void)
 	printf("   -v            verify mode (restore if corrupt)\n");
 	printf("   -n hashsize   set the new hash size for the backup\n");
 	printf("   -l            open without locking to back up mutex dbs\n");
+	printf("   -r            open with read only locking\n");
 }
 
  int main(int argc, char *argv[])
@@ -292,11 +306,12 @@ static void usage(void)
 	int verify = 0;
 	int hashsize = 0;
 	int nolock = 0;
+        int readonly = 0;
 	const char *suffix = ".bak";
 
 	log_ctx.log_fn = tdb_log;
 
-	while ((c = getopt(argc, argv, "vhs:n:l")) != -1) {
+	while ((c = getopt(argc, argv, "vhs:n:lr")) != -1) {
 		switch (c) {
 		case 'h':
 			usage();
@@ -313,6 +328,8 @@ static void usage(void)
 		case 'l':
 			nolock = 1;
 			break;
+                case 'r':
+                        readonly = 1;
 		}
 	}
 
@@ -337,7 +354,7 @@ static void usage(void)
 		} else {
 			if (file_newer(fname, bak_name) &&
 			    backup_tdb(fname, bak_name, hashsize,
-				       nolock) != 0) {
+				       nolock, readonly) != 0) {
 				ret = 1;
 			}
 		}
-- 
2.7.4


From dd46e05e9bb4fbd600a2dd2b06ffc5b397665808 Mon Sep 17 00:00:00 2001
From: Aaron <aaronhaslett at catalyst.net.nz>
Date: Fri, 23 Mar 2018 15:23:11 +1300
Subject: [PATCH 7/7] auth: keytab invalidation fix

Signed-off-by: Aaron Haslett <aaronhaslett at catalyst.net.nz>
---
 selftest/knownfail                    | 2 --
 source4/auth/kerberos/kerberos_util.c | 2 +-
 2 files changed, 1 insertion(+), 3 deletions(-)

diff --git a/selftest/knownfail b/selftest/knownfail
index c9266ab..a2aeed2 100644
--- a/selftest/knownfail
+++ b/selftest/knownfail
@@ -343,5 +343,3 @@
 # Disabling NTLM means you can't use samr to change the password
 ^samba.tests.ntlmdisabled.python\(ktest\).ntlmdisabled.NtlmDisabledTests.test_samr_change_password\(ktest\)
 ^samba.tests.ntlmdisabled.python\(ad_dc_no_ntlm\).ntlmdisabled.NtlmDisabledTests.test_ntlm_connection\(ad_dc_no_ntlm\)
-^samba.unittests.kerberos.test_krb5_remove_obsolete_keytab_entries_many
-.*samba.tests.domain_backup.DomainBackup.test_domain_backup_fail
diff --git a/source4/auth/kerberos/kerberos_util.c b/source4/auth/kerberos/kerberos_util.c
index 618da62..50bf8fe 100644
--- a/source4/auth/kerberos/kerberos_util.c
+++ b/source4/auth/kerberos/kerberos_util.c
@@ -633,7 +633,7 @@ krb5_error_code smb_krb5_remove_obsolete_keytab_entries(TALLOC_CTX *mem_ctx,
 		krb5_kt_free_entry(context, &entry);
 		/* Make sure we do not double free */
 		ZERO_STRUCT(entry);
-	} while (code != 0);
+	} while (code == 0);
 
 	krb5_kt_end_seq_get(context, keytab, &cursor);
 
-- 
2.7.4



More information about the samba-technical mailing list