summaryrefslogtreecommitdiffstats
path: root/python/samba/tests/samba_tool
diff options
context:
space:
mode:
Diffstat (limited to 'python/samba/tests/samba_tool')
-rw-r--r--python/samba/tests/samba_tool/__init__.py15
-rw-r--r--python/samba/tests/samba_tool/base.py137
-rw-r--r--python/samba/tests/samba_tool/computer.py378
-rwxr-xr-xpython/samba/tests/samba_tool/computer_edit.sh197
-rw-r--r--python/samba/tests/samba_tool/contact.py468
-rwxr-xr-xpython/samba/tests/samba_tool/contact_edit.sh183
-rw-r--r--python/samba/tests/samba_tool/demote.py106
-rw-r--r--python/samba/tests/samba_tool/dnscmd.py1506
-rw-r--r--python/samba/tests/samba_tool/domain_auth_policy.py1517
-rw-r--r--python/samba/tests/samba_tool/domain_auth_silo.py618
-rw-r--r--python/samba/tests/samba_tool/domain_claim.py608
-rw-r--r--python/samba/tests/samba_tool/domain_models.py416
-rw-r--r--python/samba/tests/samba_tool/drs_clone_dc_data_lmdb_size.py119
-rw-r--r--python/samba/tests/samba_tool/dsacl.py211
-rw-r--r--python/samba/tests/samba_tool/forest.py70
-rw-r--r--python/samba/tests/samba_tool/fsmo.py52
-rw-r--r--python/samba/tests/samba_tool/gpo.py1847
-rw-r--r--python/samba/tests/samba_tool/gpo_exts.py202
-rw-r--r--python/samba/tests/samba_tool/group.py613
-rwxr-xr-xpython/samba/tests/samba_tool/group_edit.sh228
-rw-r--r--python/samba/tests/samba_tool/help.py81
-rw-r--r--python/samba/tests/samba_tool/join.py31
-rw-r--r--python/samba/tests/samba_tool/join_lmdb_size.py152
-rw-r--r--python/samba/tests/samba_tool/join_member.py71
-rw-r--r--python/samba/tests/samba_tool/ntacl.py247
-rw-r--r--python/samba/tests/samba_tool/ou.py291
-rw-r--r--python/samba/tests/samba_tool/passwordsettings.py484
-rw-r--r--python/samba/tests/samba_tool/processes.py42
-rw-r--r--python/samba/tests/samba_tool/promote_dc_lmdb_size.py174
-rw-r--r--python/samba/tests/samba_tool/provision_lmdb_size.py132
-rw-r--r--python/samba/tests/samba_tool/provision_password_check.py57
-rw-r--r--python/samba/tests/samba_tool/provision_userPassword_crypt.py67
-rw-r--r--python/samba/tests/samba_tool/rodc.py131
-rw-r--r--python/samba/tests/samba_tool/schema.py109
-rw-r--r--python/samba/tests/samba_tool/silo_base.py229
-rw-r--r--python/samba/tests/samba_tool/sites.py205
-rw-r--r--python/samba/tests/samba_tool/timecmd.py44
-rw-r--r--python/samba/tests/samba_tool/user.py1246
-rw-r--r--python/samba/tests/samba_tool/user_auth_policy.py86
-rw-r--r--python/samba/tests/samba_tool/user_auth_silo.py84
-rw-r--r--python/samba/tests/samba_tool/user_check_password_script.py106
-rwxr-xr-xpython/samba/tests/samba_tool/user_edit.sh198
-rw-r--r--python/samba/tests/samba_tool/user_get_kerberos_ticket.py195
-rw-r--r--python/samba/tests/samba_tool/user_getpassword_gmsa.py171
-rw-r--r--python/samba/tests/samba_tool/user_virtualCryptSHA.py516
-rw-r--r--python/samba/tests/samba_tool/user_virtualCryptSHA_base.py99
-rw-r--r--python/samba/tests/samba_tool/user_virtualCryptSHA_gpg.py262
-rw-r--r--python/samba/tests/samba_tool/user_virtualCryptSHA_userPassword.py188
-rw-r--r--python/samba/tests/samba_tool/user_wdigest.py450
-rw-r--r--python/samba/tests/samba_tool/visualize.py618
-rw-r--r--python/samba/tests/samba_tool/visualize_drs.py636
51 files changed, 16893 insertions, 0 deletions
diff --git a/python/samba/tests/samba_tool/__init__.py b/python/samba/tests/samba_tool/__init__.py
new file mode 100644
index 0000000..3d7f059
--- /dev/null
+++ b/python/samba/tests/samba_tool/__init__.py
@@ -0,0 +1,15 @@
+# Unix SMB/CIFS implementation.
+# Copyright (C) Sean Dague <sdague@linux.vnet.ibm.com
+#
+# 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/>.
diff --git a/python/samba/tests/samba_tool/base.py b/python/samba/tests/samba_tool/base.py
new file mode 100644
index 0000000..a4f4578
--- /dev/null
+++ b/python/samba/tests/samba_tool/base.py
@@ -0,0 +1,137 @@
+# Unix SMB/CIFS implementation.
+# Copyright (C) Sean Dague <sdague@linux.vnet.ibm.com> 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/>.
+
+# This provides a wrapper around the cmd interface so that tests can
+# easily be built on top of it and have minimal code to run basic tests
+# of the commands. A list of the environmental variables can be found in
+# ~/selftest/selftest.pl
+#
+# These can all be accessed via os.environ["VARIABLENAME"] when needed
+
+import os
+import random
+import string
+from io import StringIO
+
+import samba.getopt as options
+import samba.tests
+from samba.auth import system_session
+from samba.getopt import OptionParser
+from samba.netcmd.main import cmd_sambatool
+from samba.samdb import SamDB
+
+
+def truncate_string(s, cutoff=100):
+ if len(s) < cutoff + 15:
+ return s
+ return s[:cutoff] + '[%d more characters]' % (len(s) - cutoff)
+
+
+class SambaToolCmdTest(samba.tests.BlackboxTestCase):
+ # Use a class level reference to StringIO, which subclasses can
+ # override if they need to (to e.g. add a lying isatty() method).
+ stringIO = StringIO
+
+ @staticmethod
+ def getSamDB(*argv):
+ """a convenience function to get a samdb instance so that we can query it"""
+
+ parser = OptionParser()
+ sambaopts = options.SambaOptions(parser)
+ credopts = options.CredentialsOptions(parser)
+ hostopts = options.HostOptions(parser)
+ parser.parse_args(list(argv))
+
+ lp = sambaopts.get_loadparm()
+ creds = credopts.get_credentials(lp, fallback_machine=True)
+
+ return SamDB(url=hostopts.H, session_info=system_session(),
+ credentials=creds, lp=lp)
+
+ @classmethod
+ def _run(cls, *argv):
+ """run a samba-tool command"""
+ cmd, args = cmd_sambatool()._resolve('samba-tool', *argv,
+ outf=cls.stringIO(),
+ errf=cls.stringIO())
+ result = cmd._run(*args)
+ return (result, cmd.outf.getvalue(), cmd.errf.getvalue())
+
+ runcmd = _run
+ runsubcmd = _run
+
+ def runsublevelcmd(self, name, sublevels, *args):
+ """run a command with any number of sub command levels"""
+ # This is a weird and clunky interface for running a
+ # subcommand. Use self.runcmd() instead.
+ return self._run(name, *sublevels, *args)
+
+ def assertCmdSuccess(self, exit, out, err, msg=""):
+ # Make sure we allow '\n]\n' in stdout and stderr
+ # without causing problems with the subunit protocol.
+ # We just inject a space...
+ msg = "exit[%s] stdout[%s] stderr[%s]: %s" % (exit, out, err, msg)
+ self.assertIsNone(exit, msg=msg.replace("\n]\n", "\n] \n"))
+
+ def assertCmdFail(self, val, msg=""):
+ self.assertIsNotNone(val, msg)
+
+ def assertMatch(self, base, string, msg=None):
+ # Note: we should stop doing this and just use self.assertIn()
+ if msg is None:
+ msg = "%r is not in %r" % (truncate_string(string),
+ truncate_string(base))
+ self.assertIn(string, base, msg)
+
+ def randomName(self, count=8):
+ """Create a random name, cap letters and numbers, and always starting with a letter"""
+ name = random.choice(string.ascii_uppercase)
+ name += ''.join(random.choice(string.ascii_uppercase + string.ascii_lowercase + string.digits) for x in range(count - 1))
+ return name
+
+ def randomXid(self):
+ # pick some unused, high UID/GID range to avoid interference
+ # from the system the test runs on
+
+ # initialize a list to store used IDs
+ try:
+ self.used_xids
+ except AttributeError:
+ self.used_xids = []
+
+ # try to get an unused ID
+ failed = 0
+ while failed < 50:
+ xid = random.randint(4711000, 4799000)
+ if xid not in self.used_xids:
+ self.used_xids += [xid]
+ return xid
+ failed += 1
+ assert False, "No Xid are available"
+
+ def assertWithin(self, val1, val2, delta, msg=""):
+ """Assert that val1 is within delta of val2, useful for time computations"""
+ self.assertTrue(((val1 + delta) > val2) and ((val1 - delta) < val2), msg)
+
+ def cleanup_join(self, netbios_name):
+ (result, out, err) \
+ = self.runsubcmd("domain",
+ "demote",
+ ("--remove-other-dead-server=%s " % netbios_name),
+ ("-U%s%%%s" % (os.environ["USERNAME"], os.environ["PASSWORD"])),
+ ("--server=%s" % os.environ["SERVER"]))
+
+ self.assertCmdSuccess(result, out, err)
diff --git a/python/samba/tests/samba_tool/computer.py b/python/samba/tests/samba_tool/computer.py
new file mode 100644
index 0000000..b60e756
--- /dev/null
+++ b/python/samba/tests/samba_tool/computer.py
@@ -0,0 +1,378 @@
+# Unix SMB/CIFS implementation.
+#
+# Copyright (C) Bjoern Baumbach <bb@sernet.de> 2018
+#
+# based on group.py:
+# Copyright (C) Michael Adam 2012
+#
+# 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 os
+import ldb
+from samba.tests.samba_tool.base import SambaToolCmdTest
+from samba import dsdb
+from samba.ndr import ndr_unpack, ndr_pack
+from samba.dcerpc import dnsp
+
+
+class ComputerCmdTestCase(SambaToolCmdTest):
+ """Tests for samba-tool computer subcommands"""
+ computers = []
+ samdb = None
+
+ def setUp(self):
+ super().setUp()
+ self.creds = "-U%s%%%s" % (os.environ["DC_USERNAME"], os.environ["DC_PASSWORD"])
+ self.samdb = self.getSamDB("-H", "ldap://%s" % os.environ["DC_SERVER"], self.creds)
+ # ips used to test --ip-address option
+ self.ipv4 = '10.10.10.10'
+ self.ipv6 = '2001:0db8:0a0b:12f0:0000:0000:0000:0001'
+ computer_basename = self.randomName().lower()
+ data = [
+ {
+ 'name': computer_basename + 'cmp1',
+ 'ip_address_list': [self.ipv4]
+ },
+ {
+ 'name': computer_basename + 'cmp2',
+ 'ip_address_list': [self.ipv6],
+ 'service_principal_name_list': [
+ 'host/' + computer_basename + 'SPN20',
+ ],
+ },
+ {
+ 'name': computer_basename + 'cmp3$',
+ 'ip_address_list': [self.ipv4, self.ipv6],
+ 'service_principal_name_list': [
+ 'host/' + computer_basename + 'SPN30',
+ 'host/' + computer_basename + 'SPN31',
+ ],
+ },
+ {
+ 'name': computer_basename + 'cmp4$',
+ },
+ ]
+ self.computers = [self._randomComputer(base=item) for item in data]
+
+ # setup the 4 computers and ensure they are correct
+ for computer in self.computers:
+ (result, out, err) = self._create_computer(computer)
+
+ self.assertCmdSuccess(result, out, err)
+ self.assertNotIn(
+ "ERROR", err, "There shouldn't be any error message")
+ self.assertIn("Computer '%s' added successfully" %
+ computer["name"], out)
+
+ found = self._find_computer(computer["name"])
+
+ self.assertIsNotNone(found)
+
+ expectedname = computer["name"].rstrip('$')
+ expectedsamaccountname = computer["name"]
+ if not computer["name"].endswith('$'):
+ expectedsamaccountname = "%s$" % computer["name"]
+ self.assertEqual("%s" % found.get("name"), expectedname)
+ self.assertEqual("%s" % found.get("sAMAccountName"),
+ expectedsamaccountname)
+ self.assertEqual("%s" % found.get("description"),
+ computer["description"])
+
+ def tearDown(self):
+ super().tearDown()
+ # clean up all the left over computers, just in case
+ for computer in self.computers:
+ if self._find_computer(computer["name"]):
+ (result, out, err) = self.runsubcmd("computer", "delete",
+ "%s" % computer["name"])
+ self.assertCmdSuccess(result, out, err,
+ "Failed to delete computer '%s'" %
+ computer["name"])
+
+ def test_newcomputer_with_service_principal_name(self):
+ # Each computer should have correct servicePrincipalName as provided.
+ for computer in self.computers:
+ expected_names = computer.get('service_principal_name_list', [])
+ found = self._find_service_principal_name(computer['name'], expected_names)
+ self.assertTrue(found)
+
+ def test_newcomputer_with_dns_records(self):
+
+ # Each computer should have correct DNS record and ip address.
+ for computer in self.computers:
+ for ip_address in computer.get('ip_address_list', []):
+ found = self._find_dns_record(computer['name'], ip_address)
+ self.assertTrue(found)
+
+ # try to delete all the computers we just created
+ for computer in self.computers:
+ (result, out, err) = self.runsubcmd("computer", "delete",
+ "%s" % computer["name"])
+ self.assertCmdSuccess(result, out, err,
+ "Failed to delete computer '%s'" %
+ computer["name"])
+ found = self._find_computer(computer["name"])
+ self.assertIsNone(found,
+ "Deleted computer '%s' still exists" %
+ computer["name"])
+
+ # all DNS records should be gone
+ for computer in self.computers:
+ for ip_address in computer.get('ip_address_list', []):
+ found = self._find_dns_record(computer['name'], ip_address)
+ self.assertFalse(found)
+
+ def test_newcomputer(self):
+ """This tests the "computer add" and "computer delete" commands"""
+ # try to create all the computers again, this should fail
+ for computer in self.computers:
+ (result, out, err) = self._create_computer(computer)
+ self.assertCmdFail(result, "Succeeded to add existing computer")
+ self.assertIn("already exists", err)
+
+ # try to delete all the computers we just added
+ for computer in self.computers:
+ (result, out, err) = self.runsubcmd("computer", "delete", "%s" %
+ computer["name"])
+ self.assertCmdSuccess(result, out, err,
+ "Failed to delete computer '%s'" %
+ computer["name"])
+ found = self._find_computer(computer["name"])
+ self.assertIsNone(found,
+ "Deleted computer '%s' still exists" %
+ computer["name"])
+
+ # test creating computers
+ for computer in self.computers:
+ (result, out, err) = self.runsubcmd(
+ "computer", "add", "%s" % computer["name"],
+ "--description=%s" % computer["description"])
+
+ self.assertCmdSuccess(result, out, err)
+ self.assertEqual(err, "", "There shouldn't be any error message")
+ self.assertIn("Computer '%s' added successfully" %
+ computer["name"], out)
+
+ found = self._find_computer(computer["name"])
+
+ expectedname = computer["name"].rstrip('$')
+ expectedsamaccountname = computer["name"]
+ if not computer["name"].endswith('$'):
+ expectedsamaccountname = "%s$" % computer["name"]
+ self.assertEqual("%s" % found.get("name"), expectedname)
+ self.assertEqual("%s" % found.get("sAMAccountName"),
+ expectedsamaccountname)
+ self.assertEqual("%s" % found.get("description"),
+ computer["description"])
+
+ def test_list(self):
+ (result, out, err) = self.runsubcmd("computer", "list")
+ self.assertCmdSuccess(result, out, err, "Error running list")
+
+ search_filter = ("(sAMAccountType=%u)" %
+ dsdb.ATYPE_WORKSTATION_TRUST)
+
+ computerlist = self.samdb.search(base=self.samdb.domain_dn(),
+ scope=ldb.SCOPE_SUBTREE,
+ expression=search_filter,
+ attrs=["samaccountname"])
+
+ self.assertTrue(len(computerlist) > 0, "no computers found in samdb")
+
+ for computerobj in computerlist:
+ name = computerobj.get("samaccountname", idx=0)
+ found = self.assertMatch(out, str(name),
+ "computer '%s' not found" % name)
+
+ def test_list_full_dn(self):
+ (result, out, err) = self.runsubcmd("computer", "list", "--full-dn")
+ self.assertCmdSuccess(result, out, err, "Error running list")
+
+ search_filter = ("(sAMAccountType=%u)" %
+ dsdb.ATYPE_WORKSTATION_TRUST)
+
+ computerlist = self.samdb.search(base=self.samdb.domain_dn(),
+ scope=ldb.SCOPE_SUBTREE,
+ expression=search_filter,
+ attrs=[])
+
+ self.assertTrue(len(computerlist) > 0, "no computers found in samdb")
+
+ for computerobj in computerlist:
+ name = computerobj.get("dn", idx=0)
+ found = self.assertMatch(out, str(name),
+ "computer '%s' not found" % name)
+
+ def test_list_base_dn(self):
+ base_dn = str(self.samdb.domain_dn())
+ (result, out, err) = self.runsubcmd("computer", "list", "-b", base_dn)
+ self.assertCmdSuccess(result, out, err, "Error running list")
+
+ search_filter = ("(sAMAccountType=%u)" %
+ dsdb.ATYPE_WORKSTATION_TRUST)
+
+ computerlist = self.samdb.search(base=base_dn,
+ scope=ldb.SCOPE_SUBTREE,
+ expression=search_filter,
+ attrs=["name"])
+
+ self.assertTrue(len(computerlist) > 0, "no computers found in samdb")
+
+ for computerobj in computerlist:
+ name = computerobj.get("name", idx=0)
+ found = self.assertMatch(out, str(name),
+ "computer '%s' not found" % name)
+
+ def test_move(self):
+ parentou = self._randomOU({"name": "parentOU"})
+ (result, out, err) = self._create_ou(parentou)
+ self.assertCmdSuccess(result, out, err)
+
+ for computer in self.computers:
+ olddn = self._find_computer(computer["name"]).get("dn")
+
+ (result, out, err) = self.runsubcmd("computer", "move",
+ "%s" % computer["name"],
+ "OU=%s" % parentou["name"])
+ self.assertCmdSuccess(result, out, err,
+ "Failed to move computer '%s'" %
+ computer["name"])
+ self.assertEqual(err, "", "There shouldn't be any error message")
+ self.assertIn('Moved computer "%s"' % computer["name"], out)
+
+ found = self._find_computer(computer["name"])
+ self.assertNotEqual(found.get("dn"), olddn,
+ ("Moved computer '%s' still exists with the "
+ "same dn" % computer["name"]))
+ computername = computer["name"].rstrip('$')
+ newexpecteddn = ldb.Dn(self.samdb,
+ "CN=%s,OU=%s,%s" %
+ (computername, parentou["name"],
+ self.samdb.domain_dn()))
+ self.assertEqual(found.get("dn"), newexpecteddn,
+ "Moved computer '%s' does not exist" %
+ computer["name"])
+
+ (result, out, err) = self.runsubcmd("computer", "move",
+ "%s" % computer["name"],
+ "%s" % olddn.parent())
+ self.assertCmdSuccess(result, out, err,
+ "Failed to move computer '%s'" %
+ computer["name"])
+
+ (result, out, err) = self.runsubcmd("ou", "delete",
+ "OU=%s" % parentou["name"])
+ self.assertCmdSuccess(result, out, err,
+ "Failed to delete ou '%s'" % parentou["name"])
+
+ def _randomComputer(self, base=None):
+ """create a computer with random attribute values, you can specify base
+ attributes"""
+ if base is None:
+ base = {}
+
+ computer = {
+ "name": self.randomName(),
+ "description": self.randomName(count=100),
+ }
+ computer.update(base)
+ return computer
+
+ def _randomOU(self, base=None):
+ """create an ou with random attribute values, you can specify base
+ attributes"""
+ if base is None:
+ base = {}
+
+ ou = {
+ "name": self.randomName(),
+ "description": self.randomName(count=100),
+ }
+ ou.update(base)
+ return ou
+
+ def _create_computer(self, computer):
+ args = '{0} {1} --description={2}'.format(
+ computer['name'], self.creds, computer["description"])
+
+ for ip_address in computer.get('ip_address_list', []):
+ args += ' --ip-address={0}'.format(ip_address)
+
+ for service_principal_name in computer.get('service_principal_name_list', []):
+ args += ' --service-principal-name={0}'.format(service_principal_name)
+
+ args = args.split()
+
+ return self.runsubcmd('computer', 'add', *args)
+
+ def _create_ou(self, ou):
+ return self.runsubcmd("ou", "add", "OU=%s" % ou["name"],
+ "--description=%s" % ou["description"])
+
+ def _find_computer(self, name):
+ samaccountname = name
+ if not name.endswith('$'):
+ samaccountname = "%s$" % name
+ search_filter = ("(&(sAMAccountName=%s)(objectCategory=%s,%s))" %
+ (ldb.binary_encode(samaccountname),
+ "CN=Computer,CN=Schema,CN=Configuration",
+ self.samdb.domain_dn()))
+ computerlist = self.samdb.search(base=self.samdb.domain_dn(),
+ scope=ldb.SCOPE_SUBTREE,
+ expression=search_filter)
+ if computerlist:
+ return computerlist[0]
+ else:
+ return None
+
+ def _find_dns_record(self, name, ip_address):
+ name = name.rstrip('$') # computername
+ records = self.samdb.search(
+ base="DC=DomainDnsZones,{0}".format(self.samdb.get_default_basedn()),
+ scope=ldb.SCOPE_SUBTREE,
+ expression="(&(objectClass=dnsNode)(name={0}))".format(name),
+ attrs=['dnsRecord', 'dNSTombstoned'])
+
+ # unpack data and compare
+ for record in records:
+ if 'dNSTombstoned' in record and str(record['dNSTombstoned']) == 'TRUE':
+ # if a record is dNSTombstoned, ignore it.
+ continue
+ for dns_record_bin in record['dnsRecord']:
+ dns_record_obj = ndr_unpack(dnsp.DnssrvRpcRecord, dns_record_bin)
+ ip = str(dns_record_obj.data)
+
+ if str(ip) == str(ip_address):
+ return True
+
+ return False
+
+ def _find_service_principal_name(self, name, expected_service_principal_names):
+ """Find all servicePrincipalName values and compare with expected_service_principal_names"""
+ samaccountname = name.strip('$') + '$'
+ search_filter = ("(&(sAMAccountName=%s)(objectCategory=%s,%s))" %
+ (ldb.binary_encode(samaccountname),
+ "CN=Computer,CN=Schema,CN=Configuration",
+ self.samdb.domain_dn()))
+ computer_list = self.samdb.search(
+ base=self.samdb.domain_dn(),
+ scope=ldb.SCOPE_SUBTREE,
+ expression=search_filter,
+ attrs=['servicePrincipalName'])
+ names = set()
+ for computer in computer_list:
+ for name in computer.get('servicePrincipalName', []):
+ names.add(str(name))
+ return names == set(expected_service_principal_names)
diff --git a/python/samba/tests/samba_tool/computer_edit.sh b/python/samba/tests/samba_tool/computer_edit.sh
new file mode 100755
index 0000000..da32760
--- /dev/null
+++ b/python/samba/tests/samba_tool/computer_edit.sh
@@ -0,0 +1,197 @@
+#!/bin/sh
+#
+# Test for 'samba-tool computer edit'
+
+if [ $# -lt 3 ]; then
+ cat <<EOF
+Usage: computer_edit.sh SERVER USERNAME PASSWORD
+EOF
+ exit 1
+fi
+
+SERVER="$1"
+USERNAME="$2"
+PASSWORD="$3"
+
+STpath=$(pwd)
+. $STpath/testprogs/blackbox/subunit.sh
+. "${STpath}/testprogs/blackbox/common_test_fns.inc"
+
+ldbsearch=$(system_or_builddir_binary ldbsearch "${BINDIR}")
+
+display_name="Björns laptop"
+display_name_b64="QmrDtnJucyBsYXB0b3A="
+display_name_new="Bjoerns new laptop"
+# attribute value including control character
+# echo -e "test \a string" | base64
+display_name_con_b64="dGVzdCAHIHN0cmluZwo="
+
+tmpeditor=$(mktemp --suffix .sh -p $SELFTEST_TMPDIR samba-tool-editor-XXXXXXXX)
+chmod +x $tmpeditor
+
+TEST_MACHINE="$(mktemp -u testmachineXXXXXX)"
+
+create_test_computer()
+{
+ $PYTHON ${STpath}/source4/scripting/bin/samba-tool \
+ computer create ${TEST_MACHINE} \
+ -H "ldap://$SERVER" "-U$USERNAME" "--password=$PASSWORD"
+}
+
+edit_computer()
+{
+ # create editor.sh
+ # enable computer account
+ cat >$tmpeditor <<-'EOF'
+#!/usr/bin/env bash
+computer_ldif="$1"
+SED=$(which sed)
+$SED -i -e 's/userAccountControl: 4098/userAccountControl: 4096/' $computer_ldif
+EOF
+
+ $PYTHON ${STpath}/source4/scripting/bin/samba-tool \
+ computer edit ${TEST_MACHINE} --editor=$tmpeditor \
+ -H "ldap://$SERVER" "-U$USERNAME" "--password=$PASSWORD"
+}
+
+# Test edit computer - add base64 attributes
+add_attribute_base64()
+{
+ # create editor.sh
+ cat >$tmpeditor <<EOF
+#!/usr/bin/env bash
+computer_ldif="\$1"
+
+grep -v '^\$' \$computer_ldif > \${computer_ldif}.tmp
+echo "displayName:: $display_name_b64" >> \${computer_ldif}.tmp
+
+mv \${computer_ldif}.tmp \$computer_ldif
+EOF
+
+ $PYTHON ${STpath}/source4/scripting/bin/samba-tool computer edit \
+ ${TEST_MACHINE} --editor=$tmpeditor \
+ -H "ldap://$SERVER" "-U$USERNAME" "--password=$PASSWORD"
+}
+
+get_attribute_base64()
+{
+ ${ldbsearch} "(sAMAccountName=${TEST_MACHINE}\$)" displayName \
+ -H "ldap://$SERVER" "-U$USERNAME" "--password=$PASSWORD"
+}
+
+delete_attribute()
+{
+ # create editor.sh
+ cat >$tmpeditor <<EOF
+#!/usr/bin/env bash
+computer_ldif="\$1"
+
+grep -v '^displayName' \$computer_ldif >> \${computer_ldif}.tmp
+mv \${computer_ldif}.tmp \$computer_ldif
+EOF
+ $PYTHON ${STpath}/source4/scripting/bin/samba-tool computer edit \
+ ${TEST_MACHINE} --editor=$tmpeditor \
+ -H "ldap://$SERVER" "-U$USERNAME" "--password=$PASSWORD"
+}
+
+# Test edit computer - add base64 attribute value including control character
+add_attribute_base64_control()
+{
+ # create editor.sh
+ cat >$tmpeditor <<EOF
+#!/usr/bin/env bash
+computer_ldif="\$1"
+
+grep -v '^\$' \$computer_ldif > \${computer_ldif}.tmp
+echo "displayName:: $display_name_con_b64" >> \${computer_ldif}.tmp
+
+mv \${computer_ldif}.tmp \$computer_ldif
+EOF
+ $PYTHON ${STpath}/source4/scripting/bin/samba-tool computer edit \
+ ${TEST_MACHINE} --editor=$tmpeditor \
+ -H "ldap://$SERVER" "-U$USERNAME" "--password=$PASSWORD"
+}
+
+get_attribute_base64_control()
+{
+ $PYTHON ${STpath}/source4/scripting/bin/samba-tool computer show \
+ ${TEST_MACHINE} --attributes=displayName \
+ -H "ldap://$SERVER" "-U$USERNAME" "--password=$PASSWORD"
+}
+
+get_attribute_force_no_base64()
+{
+ # LDB_FLAG_FORCE_NO_BASE64_LDIF should be used here.
+ $PYTHON ${STpath}/source4/scripting/bin/samba-tool computer show \
+ ${TEST_MACHINE} --attributes=displayName \
+ -H "ldap://$SERVER" "-U$USERNAME" "--password=$PASSWORD"
+}
+
+# Test edit computer - change base64 attribute value including control character
+change_attribute_base64_control()
+{
+ # create editor.sh
+ cat >$tmpeditor <<EOF
+#!/usr/bin/env bash
+computer_ldif="\$1"
+
+sed -i -e 's/displayName:: $display_name_con_b64/displayName: $display_name/' \
+ \$computer_ldif
+EOF
+ $PYTHON ${STpath}/source4/scripting/bin/samba-tool computer edit \
+ ${TEST_MACHINE} --editor=$tmpeditor \
+ -H "ldap://$SERVER" "-U$USERNAME" "--password=$PASSWORD"
+}
+
+# Test edit computer - change attributes with LDB_FLAG_FORCE_NO_BASE64_LDIF
+change_attribute_force_no_base64()
+{
+ # create editor.sh
+ # Expects that the original attribute is available as clear text,
+ # because the LDB_FLAG_FORCE_NO_BASE64_LDIF should be used here.
+ cat >$tmpeditor <<EOF
+#!/usr/bin/env bash
+computer_ldif="\$1"
+
+sed -i -e 's/displayName: $display_name/displayName: $display_name_new/' \
+ \$computer_ldif
+EOF
+
+ $PYTHON ${STpath}/source4/scripting/bin/samba-tool computer edit \
+ ${TEST_MACHINE} --editor=$tmpeditor \
+ -H "ldap://$SERVER" "-U$USERNAME" "--password=$PASSWORD"
+}
+
+get_changed_attribute_force_no_base64()
+{
+ $PYTHON ${STpath}/source4/scripting/bin/samba-tool computer show \
+ ${TEST_MACHINE} --attributes=displayName \
+ -H "ldap://$SERVER" "-U$USERNAME" "--password=$PASSWORD"
+}
+
+delete_computer()
+{
+ $PYTHON ${STpath}/source4/scripting/bin/samba-tool \
+ computer delete ${TEST_MACHINE} \
+ -H "ldap://$SERVER" "-U$USERNAME" "--password=$PASSWORD"
+}
+
+failed=0
+
+testit "create_test_computer" create_test_computer || failed=$(expr $failed + 1)
+testit "edit_computer" edit_computer || failed=$(expr $failed + 1)
+testit "add_attribute_base64" add_attribute_base64 || failed=$(expr $failed + 1)
+testit_grep "get_attribute_base64" "^displayName:: $display_name_b64" get_attribute_base64 || failed=$(expr $failed + 1)
+testit "delete_attribute" delete_attribute || failed=$(expr $failed + 1)
+testit "add_attribute_base64_control" add_attribute_base64_control || failed=$(expr $failed + 1)
+testit_grep "get_attribute_base64_control" "^displayName:: $display_name_con_b64" get_attribute_base64_control || failed=$(expr $failed + 1)
+testit "change_attribute_base64_control" change_attribute_base64_control || failed=$(expr $failed + 1)
+testit_grep "get_attribute_base64" "^displayName:: $display_name_b64" get_attribute_base64 || failed=$(expr $failed + 1)
+testit_grep "get_attribute_force_no_base64" "^displayName: $display_name" get_attribute_force_no_base64 || failed=$(expr $failed + 1)
+testit "change_attribute_force_no_base64" change_attribute_force_no_base64 || failed=$(expr $failed + 1)
+testit_grep "get_changed_attribute_force_no_base64" "^displayName: $display_name_new" get_changed_attribute_force_no_base64 || failed=$(expr $failed + 1)
+testit "delete_computer" delete_computer || failed=$(expr $failed + 1)
+
+rm -f $tmpeditor
+
+exit $failed
diff --git a/python/samba/tests/samba_tool/contact.py b/python/samba/tests/samba_tool/contact.py
new file mode 100644
index 0000000..2bec813
--- /dev/null
+++ b/python/samba/tests/samba_tool/contact.py
@@ -0,0 +1,468 @@
+# Unix SMB/CIFS implementation.
+#
+# Tests for samba-tool contact management commands
+#
+# Copyright (C) Bjoern Baumbach <bbaumbach@samba.org> 2019
+#
+# 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 os
+import ldb
+from samba.tests.samba_tool.base import SambaToolCmdTest
+
+class ContactCmdTestCase(SambaToolCmdTest):
+ """Tests for samba-tool contact subcommands"""
+ contacts = []
+ samdb = None
+
+ def setUp(self):
+ super().setUp()
+ self.creds = "-U%s%%%s" % (os.environ["DC_USERNAME"],
+ os.environ["DC_PASSWORD"])
+ self.samdb = self.getSamDB("-H",
+ "ldap://%s" % os.environ["DC_SERVER"],
+ self.creds)
+ contact = None
+ self.contacts = []
+
+ contact = self._randomContact({"expectedname": "contact1",
+ "name": "contact1"})
+ self.contacts.append(contact)
+
+ # No 'name' is given here, so the name will be made from givenname.
+ contact = self._randomContact({"expectedname": "contact2",
+ "givenName": "contact2"})
+ self.contacts.append(contact)
+
+ contact = self._randomContact({"expectedname": "contact3",
+ "name": "contact3",
+ "displayName": "contact3displayname",
+ "givenName": "not_contact3",
+ "initials": "I",
+ "sn": "not_contact3",
+ "mobile": "12345"})
+ self.contacts.append(contact)
+
+ # No 'name' is given here, so the name will be made from the the
+ # sn, initials and givenName attributes.
+ contact = self._randomContact({"expectedname": "James T. Kirk",
+ "sn": "Kirk",
+ "initials": "T",
+ "givenName": "James"})
+ self.contacts.append(contact)
+
+ # setup the 4 contacts and ensure they are correct
+ for contact in self.contacts:
+ (result, out, err) = self._create_contact(contact)
+
+ self.assertCmdSuccess(result, out, err)
+ self.assertNotIn(
+ "ERROR", err, "There shouldn't be any error message")
+ self.assertIn("Contact '%s' added successfully" %
+ contact["expectedname"], out)
+
+ found = self._find_contact(contact["expectedname"])
+
+ self.assertIsNotNone(found)
+
+ contactname = contact["expectedname"]
+ self.assertEqual("%s" % found.get("name"), contactname)
+ self.assertEqual("%s" % found.get("description"),
+ contact["description"])
+
+ def tearDown(self):
+ super().tearDown()
+ # clean up all the left over contacts, just in case
+ for contact in self.contacts:
+ if self._find_contact(contact["expectedname"]):
+ (result, out, err) = self.runsubcmd(
+ "contact", "delete", "%s" % contact["expectedname"])
+ self.assertCmdSuccess(result, out, err,
+ "Failed to delete contact '%s'" %
+ contact["expectedname"])
+
+ def test_newcontact(self):
+ """This tests the "contact create" and "contact delete" commands"""
+ # try to create all the contacts again, this should fail
+ for contact in self.contacts:
+ (result, out, err) = self._create_contact(contact)
+ self.assertCmdFail(result, "Succeeded to create existing contact")
+ self.assertIn("already exists", err)
+
+ # try to delete all the contacts we just added
+ for contact in self.contacts:
+ (result, out, err) = self.runsubcmd("contact", "delete", "%s" %
+ contact["expectedname"])
+ self.assertCmdSuccess(result, out, err,
+ "Failed to delete contact '%s'" %
+ contact["expectedname"])
+ found = self._find_contact(contact["expectedname"])
+ self.assertIsNone(found,
+ "Deleted contact '%s' still exists" %
+ contact["expectedname"])
+
+ # test creating contacts in an specified OU
+ parentou = self._randomOU({"name": "testOU"})
+ (result, out, err) = self._create_ou(parentou)
+ self.assertCmdSuccess(result, out, err)
+
+ for contact in self.contacts:
+ (result, out, err) = self._create_contact(contact, ou="OU=testOU")
+
+ self.assertCmdSuccess(result, out, err)
+ self.assertEqual(err, "", "There shouldn't be any error message")
+ self.assertIn("Contact '%s' added successfully" %
+ contact["expectedname"], out)
+
+ found = self._find_contact(contact["expectedname"])
+
+ contactname = contact["expectedname"]
+ self.assertEqual("%s" % found.get("name"), contactname)
+ self.assertEqual("%s" % found.get("description"),
+ contact["description"])
+
+ # try to delete all the contacts we just added, by DN
+ for contact in self.contacts:
+ expecteddn = ldb.Dn(self.samdb,
+ "CN=%s,OU=%s,%s" %
+ (contact["expectedname"],
+ parentou["name"],
+ self.samdb.domain_dn()))
+ (result, out, err) = self.runsubcmd("contact", "delete", "%s" %
+ expecteddn)
+ self.assertCmdSuccess(result, out, err,
+ "Failed to delete contact '%s'" %
+ contact["expectedname"])
+ found = self._find_contact(contact["expectedname"])
+ self.assertIsNone(found,
+ "Deleted contact '%s' still exists" %
+ contact["expectedname"])
+
+ (result, out, err) = self.runsubcmd("ou", "delete",
+ "OU=%s" % parentou["name"])
+ self.assertCmdSuccess(result, out, err,
+ "Failed to delete ou '%s'" % parentou["name"])
+
+ # creating contacts, again for further tests
+ for contact in self.contacts:
+ (result, out, err) = self._create_contact(contact)
+
+ self.assertCmdSuccess(result, out, err)
+ self.assertEqual(err, "", "There shouldn't be any error message")
+ self.assertIn("Contact '%s' added successfully" %
+ contact["expectedname"], out)
+
+ found = self._find_contact(contact["expectedname"])
+
+ contactname = contact["expectedname"]
+ self.assertEqual("%s" % found.get("name"), contactname)
+ self.assertEqual("%s" % found.get("description"),
+ contact["description"])
+
+ def test_list(self):
+ (result, out, err) = self.runsubcmd("contact", "list")
+ self.assertCmdSuccess(result, out, err, "Error running list")
+
+ search_filter = "(objectClass=contact)"
+ contactlist = self.samdb.search(base=self.samdb.domain_dn(),
+ scope=ldb.SCOPE_SUBTREE,
+ expression=search_filter,
+ attrs=["name"])
+
+ self.assertTrue(len(contactlist) > 0, "no contacts found in samdb")
+
+ for contactobj in contactlist:
+ name = contactobj.get("name", idx=0)
+ self.assertMatch(out, str(name),
+ "contact '%s' not found" % name)
+
+ def test_list_full_dn(self):
+ (result, out, err) = self.runsubcmd("contact", "list", "--full-dn")
+ self.assertCmdSuccess(result, out, err, "Error running list")
+
+ search_filter = "(objectClass=contact)"
+ contactlist = self.samdb.search(base=self.samdb.domain_dn(),
+ scope=ldb.SCOPE_SUBTREE,
+ expression=search_filter,
+ attrs=["dn"])
+
+ self.assertTrue(len(contactlist) > 0, "no contacts found in samdb")
+
+ for contactobj in contactlist:
+ self.assertMatch(out, str(contactobj.dn),
+ "contact '%s' not found" % str(contactobj.dn))
+
+ def test_list_base_dn(self):
+ base_dn = str(self.samdb.domain_dn())
+ (result, out, err) = self.runsubcmd("contact", "list",
+ "-b", base_dn)
+ self.assertCmdSuccess(result, out, err, "Error running list")
+
+ search_filter = "(objectClass=contact)"
+ contactlist = self.samdb.search(base=base_dn,
+ scope=ldb.SCOPE_SUBTREE,
+ expression=search_filter,
+ attrs=["name"])
+
+ self.assertTrue(len(contactlist) > 0, "no contacts found in samdb")
+
+ for contactobj in contactlist:
+ name = contactobj.get("name", idx=0)
+ self.assertMatch(out, str(name),
+ "contact '%s' not found" % name)
+
+ def test_move(self):
+ parentou = self._randomOU({"name": "parentOU"})
+ (result, out, err) = self._create_ou(parentou)
+ self.assertCmdSuccess(result, out, err)
+
+ for contact in self.contacts:
+ olddn = self._find_contact(contact["expectedname"]).get("dn")
+
+ (result, out, err) = self.runsubcmd("contact", "move",
+ "%s" % contact["expectedname"],
+ "OU=%s" % parentou["name"])
+ self.assertCmdSuccess(result, out, err,
+ "Failed to move contact '%s'" %
+ contact["expectedname"])
+ self.assertEqual(err, "", "There shouldn't be any error message")
+ self.assertIn('Moved contact "%s"' % contact["expectedname"], out)
+
+ found = self._find_contact(contact["expectedname"])
+ self.assertNotEqual(found.get("dn"), olddn,
+ ("Moved contact '%s' still exists with the "
+ "same dn" % contact["expectedname"]))
+ contactname = contact["expectedname"]
+ newexpecteddn = ldb.Dn(self.samdb,
+ "CN=%s,OU=%s,%s" %
+ (contactname,
+ parentou["name"],
+ self.samdb.domain_dn()))
+ self.assertEqual(found.get("dn"), newexpecteddn,
+ "Moved contact '%s' does not exist" %
+ contact["expectedname"])
+
+ (result, out, err) = self.runsubcmd("contact", "move",
+ "%s" % contact["expectedname"],
+ "%s" % olddn.parent())
+ self.assertCmdSuccess(result, out, err,
+ "Failed to move contact '%s'" %
+ contact["expectedname"])
+
+ (result, out, err) = self.runsubcmd("ou", "delete",
+ "OU=%s" % parentou["name"])
+ self.assertCmdSuccess(result, out, err,
+ "Failed to delete ou '%s'" % parentou["name"])
+
+ def test_rename_givenname_initials_surname(self):
+ """rename and remove given name, initials and surname for all contacts"""
+ for contact in self.contacts:
+ name = contact["name"] if "name" in contact else contact["expectedname"]
+
+ new_givenname = "new_given_name_of_" + name
+ new_initials = "A"
+ new_surname = "new_surname_of_" + name
+ new_cn = "new_cn_of_" + name
+ expected_cn = "%s %s. %s" % (new_givenname, new_initials, new_surname)
+
+ # rename given name, initials and surname
+ (result, out, err) = self.runsubcmd("contact", "rename", name,
+ "--reset-cn",
+ "--surname=%s" % new_surname,
+ "--initials=%s" % new_initials,
+ "--given-name=%s" % new_givenname)
+ self.assertCmdSuccess(result, out, err)
+ self.assertEqual(err, "", "Shouldn't be any error messages")
+ self.assertIn('successfully', out)
+
+ found = self._find_contact(expected_cn)
+ self.assertEqual("%s" % found.get("givenName"), new_givenname)
+ self.assertEqual("%s" % found.get("initials"), new_initials)
+ self.assertEqual("%s" % found.get("sn"), new_surname)
+ self.assertEqual("%s" % found.get("name"), expected_cn)
+ self.assertEqual("%s" % found.get("cn"), expected_cn)
+
+ # remove given name, initials and surname
+ # (must force new cn, because en empty new CN throws an error)
+ (result, out, err) = self.runsubcmd("contact", "rename", expected_cn,
+ "--force-new-cn=%s" % expected_cn,
+ "--surname=",
+ "--initials=",
+ "--given-name=")
+ self.assertCmdSuccess(result, out, err)
+ self.assertEqual(err, "", "Shouldn't be any error messages")
+ self.assertIn('successfully', out)
+
+ found = self._find_contact(expected_cn)
+ self.assertEqual(found.get("givenName"), None)
+ self.assertEqual(found.get("initials"), None)
+ self.assertEqual(found.get("sn"), None)
+
+ # reset changes (initials are already removed)
+ old_surname = contact["sn"] if "sn" in contact else ""
+ old_initials = contact["initials"] if "initials" in contact else ""
+ old_givenname = contact["givenName"] if "givenName" in contact else ""
+ old_cn = contact["cn"] if "cn" in contact else name
+ (result, out, err) = self.runsubcmd("contact", "rename", expected_cn,
+ "--force-new-cn=%s" % old_cn,
+ "--surname=%s" % old_surname,
+ "--initials=%s" % old_initials,
+ "--given-name=%s" % old_givenname)
+ self.assertCmdSuccess(result, out, err)
+
+ def test_rename_cn(self):
+ """rename and try to remove the cn of all contacts"""
+ for contact in self.contacts:
+ name = contact["name"] if "name" in contact else contact["expectedname"]
+ new_cn = "new_cn_of_" + name
+
+ # rename cn
+ (result, out, err) = self.runsubcmd("contact", "rename", name,
+ "--force-new-cn=%s" % new_cn)
+ self.assertCmdSuccess(result, out, err)
+ self.assertEqual(err, "", "Shouldn't be any error messages")
+ self.assertIn('successfully', out)
+
+ found = self._find_contact(new_cn)
+ self.assertEqual("%s" % found.get("cn"), new_cn)
+
+ # trying to remove cn (throws an error)
+ (result, out, err) = self.runsubcmd("contact", "rename", new_cn,
+ "--force-new-cn=")
+ self.assertCmdFail(result)
+ self.assertIn('Failed to rename contact', err)
+ self.assertIn("delete protected attribute", err)
+
+ # reset changes (cn must be the name)
+ (result, out, err) = self.runsubcmd("contact", "rename", new_cn,
+ "--force-new-cn=%s" % name)
+ self.assertCmdSuccess(result, out, err)
+
+
+ def test_rename_mailaddress_displayname(self):
+ """rename and remove the mail and the displayname attribute of all contacts"""
+ for contact in self.contacts:
+ name = contact["name"] if "name" in contact else contact["expectedname"]
+ new_mail = "new_mailaddress_of_" + name
+ new_displayname = "new displayname of " + name
+
+ # change mail and displayname
+ (result, out, err) = self.runsubcmd("contact", "rename", name,
+ "--mail-address=%s"
+ % new_mail,
+ "--display-name=%s"
+ % new_displayname)
+ self.assertCmdSuccess(result, out, err)
+ self.assertEqual(err, "", "Shouldn't be any error messages")
+ self.assertIn('successfully', out)
+
+ found = self._find_contact(name)
+ self.assertEqual("%s" % found.get("mail"), new_mail)
+ self.assertEqual("%s" % found.get("displayName"), new_displayname)
+
+ # remove mail and displayname
+ (result, out, err) = self.runsubcmd("contact", "rename", name,
+ "--mail-address=",
+ "--display-name=")
+ self.assertCmdSuccess(result, out, err)
+ self.assertEqual(err, "", "Shouldn't be any error messages")
+ self.assertIn('successfully', out)
+
+ found = self._find_contact(name)
+ self.assertEqual(found.get("mail"), None)
+ self.assertEqual(found.get("displayName"), None)
+
+ # reset changes
+ old_mail = contact["givenName"] if "givenName" in contact else ""
+ old_displayname = contact["cn"] if "cn" in contact else ""
+ (result, out, err) = self.runsubcmd("contact", "rename", name,
+ "--mail-address=%s" % old_mail,
+ "--display-name=%s" % old_displayname)
+ self.assertCmdSuccess(result, out, err)
+
+ def _randomContact(self, base=None):
+ """Create a contact with random attribute values, you can specify base
+ attributes"""
+ if base is None:
+ base = {}
+
+ # No name attributes are given here, because the object name will
+ # be made from the sn, givenName and initials attributes, if no name
+ # is given.
+ contact = {
+ "description": self.randomName(count=100),
+ }
+ contact.update(base)
+ return contact
+
+ def _randomOU(self, base=None):
+ """Create an ou with random attribute values, you can specify base
+ attributes."""
+ if base is None:
+ base = {}
+
+ ou = {
+ "name": self.randomName(),
+ "description": self.randomName(count=100),
+ }
+ ou.update(base)
+ return ou
+
+ def _create_contact(self, contact, ou=None):
+ args = ""
+
+ if "name" in contact:
+ args += '{0}'.format(contact['name'])
+
+ args += ' {0}'.format(self.creds)
+
+ if ou is not None:
+ args += ' --ou={0}'.format(ou)
+
+ if "description" in contact:
+ args += ' --description={0}'.format(contact["description"])
+ if "sn" in contact:
+ args += ' --surname={0}'.format(contact["sn"])
+ if "initials" in contact:
+ args += ' --initials={0}'.format(contact["initials"])
+ if "givenName" in contact:
+ args += ' --given-name={0}'.format(contact["givenName"])
+ if "displayName" in contact:
+ args += ' --display-name={0}'.format(contact["displayName"])
+ if "mobile" in contact:
+ args += ' --mobile-number={0}'.format(contact["mobile"])
+
+ args = args.split()
+
+ return self.runsubcmd('contact', 'create', *args)
+
+ def _create_ou(self, ou):
+ return self.runsubcmd("ou",
+ "create",
+ "OU=%s" % ou["name"],
+ "--description=%s" % ou["description"])
+
+ def _find_contact(self, name):
+ contactname = name
+ search_filter = ("(&(objectClass=contact)(name=%s))" %
+ ldb.binary_encode(contactname))
+ contactlist = self.samdb.search(base=self.samdb.domain_dn(),
+ scope=ldb.SCOPE_SUBTREE,
+ expression=search_filter,
+ attrs=[])
+ if contactlist:
+ return contactlist[0]
+ else:
+ return None
diff --git a/python/samba/tests/samba_tool/contact_edit.sh b/python/samba/tests/samba_tool/contact_edit.sh
new file mode 100755
index 0000000..d31413d
--- /dev/null
+++ b/python/samba/tests/samba_tool/contact_edit.sh
@@ -0,0 +1,183 @@
+#!/bin/sh
+#
+# Test for 'samba-tool contact edit'
+
+if [ $# -lt 3 ]; then
+ cat <<EOF
+Usage: contact_edit.sh SERVER USERNAME PASSWORD
+EOF
+ exit 1
+fi
+
+SERVER="$1"
+USERNAME="$2"
+PASSWORD="$3"
+
+samba_ldbsearch=ldbsearch
+if test -x $BINDIR/ldbsearch; then
+ samba_ldbsearch=$BINDIR/ldbsearch
+fi
+
+STpath=$(pwd)
+. $STpath/testprogs/blackbox/subunit.sh
+
+display_name="Björn"
+display_name_b64="QmrDtnJu"
+display_name_new="Renamed Bjoern"
+# attribute value including control character
+# echo -e "test \a string" | base64
+display_name_con_b64="dGVzdCAHIHN0cmluZwo="
+
+TEST_USER="$(mktemp -u testcontactXXXXXX)"
+
+tmpeditor=$(mktemp --suffix .sh -p $SELFTEST_TMPDIR samba-tool-editor-XXXXXXXX)
+chmod +x $tmpeditor
+
+create_test_contact()
+{
+ $PYTHON ${STpath}/source4/scripting/bin/samba-tool \
+ contact create ${TEST_USER} \
+ -H "ldap://$SERVER" "-U$USERNAME" "--password=$PASSWORD"
+}
+
+# Test edit contact - add base64 attributes
+add_attribute_base64()
+{
+ # create editor.sh
+ cat >$tmpeditor <<EOF
+#!/usr/bin/env bash
+contact_ldif="\$1"
+
+grep -v '^\$' \$contact_ldif > \${contact_ldif}.tmp
+echo "displayName:: $display_name_b64" >> \${contact_ldif}.tmp
+
+mv \${contact_ldif}.tmp \$contact_ldif
+EOF
+
+ $PYTHON ${STpath}/source4/scripting/bin/samba-tool contact edit \
+ ${TEST_USER} --editor=$tmpeditor \
+ -H "ldap://$SERVER" "-U$USERNAME" "--password=$PASSWORD"
+}
+
+get_attribute_base64()
+{
+ $samba_ldbsearch "(&(objectClass=contact)(name=${TEST_USER}))" \
+ displayName \
+ -H "ldap://$SERVER" "-U$USERNAME" "--password=$PASSWORD"
+}
+
+delete_attribute()
+{
+ # create editor.sh
+ cat >$tmpeditor <<EOF
+#!/usr/bin/env bash
+contact_ldif="\$1"
+
+grep -v '^displayName' \$contact_ldif >> \${contact_ldif}.tmp
+mv \${contact_ldif}.tmp \$contact_ldif
+EOF
+ $PYTHON ${STpath}/source4/scripting/bin/samba-tool contact edit \
+ ${TEST_USER} --editor=$tmpeditor \
+ -H "ldap://$SERVER" "-U$USERNAME" "--password=$PASSWORD"
+}
+
+# Test edit contact - add base64 attribute value including control character
+add_attribute_base64_control()
+{
+ # create editor.sh
+ cat >$tmpeditor <<EOF
+#!/usr/bin/env bash
+contact_ldif="\$1"
+
+grep -v '^\$' \$contact_ldif > \${contact_ldif}.tmp
+echo "displayName:: $display_name_con_b64" >> \${contact_ldif}.tmp
+
+mv \${contact_ldif}.tmp \$contact_ldif
+EOF
+ $PYTHON ${STpath}/source4/scripting/bin/samba-tool contact edit \
+ ${TEST_USER} --editor=$tmpeditor \
+ -H "ldap://$SERVER" "-U$USERNAME" "--password=$PASSWORD"
+}
+
+get_attribute_base64_control()
+{
+ $PYTHON ${STpath}/source4/scripting/bin/samba-tool contact show \
+ ${TEST_USER} --attributes=displayName \
+ -H "ldap://$SERVER" "-U$USERNAME" "--password=$PASSWORD"
+}
+
+get_attribute_force_no_base64()
+{
+ # LDB_FLAG_FORCE_NO_BASE64_LDIF should be used here.
+ $PYTHON ${STpath}/source4/scripting/bin/samba-tool contact show \
+ ${TEST_USER} --attributes=displayName \
+ -H "ldap://$SERVER" "-U$USERNAME" "--password=$PASSWORD"
+}
+
+# Test edit contact - change base64 attribute value including control character
+change_attribute_base64_control()
+{
+ # create editor.sh
+ cat >$tmpeditor <<EOF
+#!/usr/bin/env bash
+contact_ldif="\$1"
+
+sed -i -e 's/displayName:: $display_name_con_b64/displayName: $display_name/' \
+ \$contact_ldif
+EOF
+ $PYTHON ${STpath}/source4/scripting/bin/samba-tool contact edit \
+ ${TEST_USER} --editor=$tmpeditor \
+ -H "ldap://$SERVER" "-U$USERNAME" "--password=$PASSWORD"
+}
+
+# Test edit contact - change attributes with LDB_FLAG_FORCE_NO_BASE64_LDIF
+change_attribute_force_no_base64()
+{
+ # create editor.sh
+ # Expects that the original attribute is available as clear text,
+ # because the LDB_FLAG_FORCE_NO_BASE64_LDIF should be used here.
+ cat >$tmpeditor <<EOF
+#!/usr/bin/env bash
+contact_ldif="\$1"
+
+sed -i -e 's/displayName: $display_name/displayName: $display_name_new/' \
+ \$contact_ldif
+EOF
+
+ $PYTHON ${STpath}/source4/scripting/bin/samba-tool contact edit \
+ ${TEST_USER} --editor=$tmpeditor \
+ -H "ldap://$SERVER" "-U$USERNAME" "--password=$PASSWORD"
+}
+
+get_changed_attribute_force_no_base64()
+{
+ $PYTHON ${STpath}/source4/scripting/bin/samba-tool contact show \
+ ${TEST_USER} --attributes=displayName \
+ -H "ldap://$SERVER" "-U$USERNAME" "--password=$PASSWORD"
+}
+
+delete_contact()
+{
+ $PYTHON ${STpath}/source4/scripting/bin/samba-tool \
+ contact delete ${TEST_USER} \
+ -H "ldap://$SERVER" "-U$USERNAME" "--password=$PASSWORD"
+}
+
+failed=0
+
+testit "create_test_contact" create_test_contact || failed=$(expr $failed + 1)
+testit "add_attribute_base64" add_attribute_base64 || failed=$(expr $failed + 1)
+testit_grep "get_attribute_base64" "^displayName:: $display_name_b64" get_attribute_base64 || failed=$(expr $failed + 1)
+testit "delete_attribute" delete_attribute || failed=$(expr $failed + 1)
+testit "add_attribute_base64_control" add_attribute_base64_control || failed=$(expr $failed + 1)
+testit_grep "get_attribute_base64_control" "^displayName:: $display_name_con_b64" get_attribute_base64_control || failed=$(expr $failed + 1)
+testit "change_attribute_base64_control" change_attribute_base64_control || failed=$(expr $failed + 1)
+testit_grep "get_attribute_base64" "^displayName:: $display_name_b64" get_attribute_base64 || failed=$(expr $failed + 1)
+testit_grep "get_attribute_force_no_base64" "^displayName: $display_name" get_attribute_force_no_base64 || failed=$(expr $failed + 1)
+testit "change_attribute_force_no_base64" change_attribute_force_no_base64 || failed=$(expr $failed + 1)
+testit_grep "get_changed_attribute_force_no_base64" "^displayName: $display_name_new" get_changed_attribute_force_no_base64 || failed=$(expr $failed + 1)
+testit "delete_contact" delete_contact || failed=$(expr $failed + 1)
+
+rm -f $tmpeditor
+
+exit $failed
diff --git a/python/samba/tests/samba_tool/demote.py b/python/samba/tests/samba_tool/demote.py
new file mode 100644
index 0000000..2c63cca
--- /dev/null
+++ b/python/samba/tests/samba_tool/demote.py
@@ -0,0 +1,106 @@
+# Unix SMB/CIFS implementation.
+# Copyright (C) Andrew Bartlett <abartlet@samba.org> 2018
+# Written by Joe Guo <joeg@catalyst.net.nz>
+#
+# 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 os
+from samba.tests.samba_tool.base import SambaToolCmdTest
+
+
+class DemoteCmdTestCase(SambaToolCmdTest):
+ """Test for samba-tool domain demote subcommand"""
+
+ def setUp(self):
+ super().setUp()
+ self.creds_string = "-U{0}%{1}".format(
+ os.environ["DC_USERNAME"], os.environ["DC_PASSWORD"])
+
+ self.dc_server = os.environ['DC_SERVER']
+ self.dburl = "ldap://%s" % os.environ["DC_SERVER"]
+ self.samdb = self.getSamDB("-H", self.dburl, self.creds_string)
+
+ def test_demote_and_remove_dns(self):
+ """
+ Test domain demote command will also remove dns references
+ """
+
+ server = os.environ['SERVER'] # the server to demote
+ zone = os.environ['REALM'].lower()
+
+ # make sure zone exist
+ result, out, err = self.runsubcmd(
+ "dns", "zoneinfo", server, zone, self.creds_string)
+ self.assertCmdSuccess(result, out, err)
+
+ # add a A record for the server to demote
+ result, out, err = self.runsubcmd(
+ "dns", "add", self.dc_server, zone,
+ server, "A", "192.168.0.193", self.creds_string)
+ self.assertCmdSuccess(result, out, err)
+
+ # make sure above A record exist
+ result, out, err = self.runsubcmd(
+ "dns", "query", self.dc_server, zone,
+ server, 'A', self.creds_string)
+ self.assertCmdSuccess(result, out, err)
+
+ # the above A record points to this host
+ dnshostname = '{0}.{1}'.format(server, zone)
+
+ # add a SRV record points to above host
+ srv_record = "{0} 65530 65530 65530".format(dnshostname)
+ self.runsubcmd(
+ "dns", "add", self.dc_server, zone, 'testrecord', "SRV",
+ srv_record, self.creds_string)
+
+ # make sure above SRV record exist
+ result, out, err = self.runsubcmd(
+ "dns", "query", self.dc_server, zone,
+ "testrecord", 'SRV', self.creds_string)
+ self.assertCmdSuccess(result, out, err)
+
+ for type_ in ['CNAME', 'NS', 'PTR']:
+ # create record
+ self.runsubcmd(
+ "dns", "add", self.dc_server, zone,
+ 'testrecord', type_, dnshostname,
+ self.creds_string)
+ self.assertCmdSuccess(result, out, err)
+
+ # check exist
+ result, out, err = self.runsubcmd(
+ "dns", "query", self.dc_server, zone,
+ "testrecord", 'SRV', self.creds_string)
+ self.assertCmdSuccess(result, out, err)
+
+ # now demote
+ result, out, err = self.runsubcmd(
+ "domain", "demote",
+ "--server", self.dc_server,
+ "--configfile", os.environ["CONFIGFILE"],
+ "--workgroup", os.environ["DOMAIN"],
+ self.creds_string)
+ self.assertCmdSuccess(result, out, err)
+
+ result, out, err = self.runsubcmd(
+ "dns", "query", self.dc_server, zone,
+ server, 'ALL', self.creds_string)
+ self.assertCmdFail(result)
+
+ result, out, err = self.runsubcmd(
+ "dns", "query", self.dc_server, zone,
+ "testrecord", 'ALL', self.creds_string)
+ self.assertCmdFail(result)
diff --git a/python/samba/tests/samba_tool/dnscmd.py b/python/samba/tests/samba_tool/dnscmd.py
new file mode 100644
index 0000000..d372bc5
--- /dev/null
+++ b/python/samba/tests/samba_tool/dnscmd.py
@@ -0,0 +1,1506 @@
+# Unix SMB/CIFS implementation.
+# Copyright (C) Andrew Bartlett <abartlet@catalyst.net.nz>
+#
+# 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 os
+import ldb
+import re
+
+from samba.ndr import ndr_unpack, ndr_pack
+from samba.dcerpc import dnsp
+from samba.tests.samba_tool.base import SambaToolCmdTest
+import time
+from samba import dsdb_dns
+
+
+class DnsCmdTestCase(SambaToolCmdTest):
+ def setUp(self):
+ super().setUp()
+
+ self.dburl = "ldap://%s" % os.environ["SERVER"]
+ self.creds_string = "-U%s%%%s" % (os.environ["DC_USERNAME"],
+ os.environ["DC_PASSWORD"])
+
+ self.samdb = self.getSamDB("-H", self.dburl, self.creds_string)
+ self.config_dn = str(self.samdb.get_config_basedn())
+
+ self.testip = "192.168.0.193"
+ self.testip2 = "192.168.0.194"
+
+ self.addCleanup(self.deleteZone)
+ self.addZone()
+
+ # Note: SOA types don't work (and shouldn't), as we only have one zone per DNS record.
+
+ good_dns = ["SAMDOM.EXAMPLE.COM",
+ "1.EXAMPLE.COM",
+ "%sEXAMPLE.COM" % ("1." * 100),
+ "EXAMPLE",
+ "!@#$%^&*()_",
+ "HIGH\xFFBYTE",
+ "@.EXAMPLE.COM",
+ "."]
+ bad_dns = ["...",
+ ".EXAMPLE.COM",
+ ".EXAMPLE.",
+ "",
+ "SAMDOM..EXAMPLE.COM"]
+
+ good_mx = ["SAMDOM.EXAMPLE.COM 65530",
+ "SAMDOM.EXAMPLE.COM 0"]
+ bad_mx = ["SAMDOM.EXAMPLE.COM -1",
+ "SAMDOM.EXAMPLE.COM",
+ " ",
+ "SAMDOM.EXAMPLE.COM 1 1",
+ "SAMDOM.EXAMPLE.COM SAMDOM.EXAMPLE.COM"]
+
+ good_srv = ["SAMDOM.EXAMPLE.COM 65530 65530 65530",
+ "SAMDOM.EXAMPLE.COM 1 1 1"]
+ bad_srv = ["SAMDOM.EXAMPLE.COM 0 65536 0",
+ "SAMDOM.EXAMPLE.COM 0 0 65536",
+ "SAMDOM.EXAMPLE.COM 65536 0 0"]
+
+ for bad_dn in bad_dns:
+ bad_mx.append("%s 1" % bad_dn)
+ bad_srv.append("%s 0 0 0" % bad_dn)
+ for good_dn in good_dns:
+ good_mx.append("%s 1" % good_dn)
+ good_srv.append("%s 0 0 0" % good_dn)
+
+ self.good_records = {
+ "A":["192.168.0.1", "255.255.255.255"],
+ "AAAA":["1234:5678:9ABC:DEF0:0000:0000:0000:0000",
+ "0000:0000:0000:0000:0000:0000:0000:0000",
+ "1234:5678:9ABC:DEF0:1234:5678:9ABC:DEF0",
+ "1234:1234:1234::",
+ "1234:5678:9ABC:DEF0::",
+ "0000:0000::0000",
+ "1234::5678:9ABC:0000:0000:0000:0000",
+ "::1",
+ "::",
+ "1:1:1:1:1:1:1:1"],
+ "PTR": good_dns,
+ "CNAME": good_dns,
+ "NS": good_dns,
+ "MX": good_mx,
+ "SRV": good_srv,
+ "TXT": ["text", "", "@#!", "\n"]
+ }
+
+ self.bad_records = {
+ "A":["192.168.0.500",
+ "255.255.255.255/32"],
+ "AAAA":["GGGG:1234:5678:9ABC:0000:0000:0000:0000",
+ "0000:0000:0000:0000:0000:0000:0000:0000/1",
+ "AAAA:AAAA:AAAA:AAAA:G000:0000:0000:1234",
+ "1234:5678:9ABC:DEF0:1234:5678:9ABC:DEF0:1234",
+ "1234:5678:9ABC:DEF0:1234:5678:9ABC",
+ "1111::1111::1111"],
+ "PTR": bad_dns,
+ "CNAME": bad_dns,
+ "NS": bad_dns,
+ "MX": bad_mx,
+ "SRV": bad_srv
+ }
+
+ def resetZone(self):
+ self.deleteZone()
+ self.addZone()
+
+ def addZone(self):
+ self.zone = "zone"
+ result, out, err = self.runsubcmd("dns",
+ "zonecreate",
+ os.environ["SERVER"],
+ self.zone,
+ self.creds_string)
+ self.assertCmdSuccess(result, out, err)
+
+ def deleteZone(self):
+ result, out, err = self.runsubcmd("dns",
+ "zonedelete",
+ os.environ["SERVER"],
+ self.zone,
+ self.creds_string)
+ self.assertCmdSuccess(result, out, err)
+
+ def get_all_records(self, zone_name):
+ zone_dn = (f"DC={zone_name},CN=MicrosoftDNS,DC=DomainDNSZones,"
+ f"{self.samdb.get_default_basedn()}")
+
+ expression = "(&(objectClass=dnsNode)(!(dNSTombstoned=TRUE)))"
+
+ nodes = self.samdb.search(base=zone_dn, scope=ldb.SCOPE_SUBTREE,
+ expression=expression,
+ attrs=["dnsRecord", "name"])
+
+ record_map = {}
+ for node in nodes:
+ name = node["name"][0].decode()
+ record_map[name] = list(node["dnsRecord"])
+
+ return record_map
+
+ def get_record_from_db(self, zone_name, record_name):
+ zones = self.samdb.search(base="DC=DomainDnsZones,%s"
+ % self.samdb.get_default_basedn(),
+ scope=ldb.SCOPE_SUBTREE,
+ expression="(objectClass=dnsZone)",
+ attrs=["cn"])
+
+ for zone in zones:
+ if zone_name in str(zone.dn):
+ zone_dn = zone.dn
+ break
+
+ records = self.samdb.search(base=zone_dn, scope=ldb.SCOPE_SUBTREE,
+ expression="(objectClass=dnsNode)",
+ attrs=["dnsRecord"])
+
+ for old_packed_record in records:
+ if record_name in str(old_packed_record.dn):
+ return (old_packed_record.dn,
+ ndr_unpack(dnsp.DnssrvRpcRecord,
+ old_packed_record["dnsRecord"][0]))
+
+ def test_rank_none(self):
+ record_str = "192.168.50.50"
+ record_type_str = "A"
+
+ result, out, err = self.runsubcmd("dns", "add", os.environ["SERVER"],
+ self.zone, "testrecord", record_type_str,
+ record_str, self.creds_string)
+ self.assertCmdSuccess(result, out, err,
+ "Failed to add record '%s' with type %s."
+ % (record_str, record_type_str))
+
+ dn, record = self.get_record_from_db(self.zone, "testrecord")
+ record.rank = 0 # DNS_RANK_NONE
+ res = self.samdb.dns_replace_by_dn(dn, [record])
+ if res is not None:
+ self.fail("Unable to update dns record to have DNS_RANK_NONE.")
+
+ errors = []
+
+ # The record should still exist
+ result, out, err = self.runsubcmd("dns", "query", os.environ["SERVER"],
+ self.zone, "testrecord", record_type_str,
+ self.creds_string)
+ try:
+ self.assertCmdSuccess(result, out, err,
+ "Failed to query for a record"
+ "which had DNS_RANK_NONE.")
+ self.assertTrue("testrecord" in out and record_str in out,
+ "Query for a record which had DNS_RANK_NONE"
+ "succeeded but produced no resulting records.")
+ except AssertionError:
+ # Windows produces no resulting records
+ pass
+
+ # We should not be able to add a duplicate
+ result, out, err = self.runsubcmd("dns", "add", os.environ["SERVER"],
+ self.zone, "testrecord", record_type_str,
+ record_str, self.creds_string)
+ try:
+ self.assertCmdFail(result, "Successfully added duplicate record"
+ "of one which had DNS_RANK_NONE.")
+ except AssertionError as e:
+ errors.append(e)
+
+ # We should be able to delete it
+ result, out, err = self.runsubcmd("dns", "delete", os.environ["SERVER"],
+ self.zone, "testrecord", record_type_str,
+ record_str, self.creds_string)
+ try:
+ self.assertCmdSuccess(result, out, err, "Failed to delete record"
+ "which had DNS_RANK_NONE.")
+ except AssertionError as e:
+ errors.append(e)
+
+ # Now the record should not exist
+ result, out, err = self.runsubcmd("dns", "query", os.environ["SERVER"],
+ self.zone, "testrecord",
+ record_type_str, self.creds_string)
+ try:
+ self.assertCmdFail(result, "Successfully queried for deleted record"
+ "which had DNS_RANK_NONE.")
+ except AssertionError as e:
+ errors.append(e)
+
+ if len(errors) > 0:
+ err_str = "Failed appropriate behaviour with DNS_RANK_NONE:"
+ for error in errors:
+ err_str = err_str + "\n" + str(error)
+ raise AssertionError(err_str)
+
+ def test_accept_valid_commands(self):
+ """
+ For all good records, attempt to add, query and delete them.
+ """
+ num_failures = 0
+ failure_msgs = []
+ for dnstype in self.good_records:
+ for record in self.good_records[dnstype]:
+ try:
+ result, out, err = self.runsubcmd("dns", "add",
+ os.environ["SERVER"],
+ self.zone, "testrecord",
+ dnstype, record,
+ self.creds_string)
+ self.assertCmdSuccess(result, out, err, "Failed to add"
+ "record %s with type %s."
+ % (record, dnstype))
+
+ result, out, err = self.runsubcmd("dns", "query",
+ os.environ["SERVER"],
+ self.zone, "testrecord",
+ dnstype,
+ self.creds_string)
+ self.assertCmdSuccess(result, out, err, "Failed to query"
+ "record %s with qualifier %s."
+ % (record, dnstype))
+
+ result, out, err = self.runsubcmd("dns", "delete",
+ os.environ["SERVER"],
+ self.zone, "testrecord",
+ dnstype, record,
+ self.creds_string)
+ self.assertCmdSuccess(result, out, err, "Failed to remove"
+ "record %s with type %s."
+ % (record, dnstype))
+ except AssertionError as e:
+ num_failures = num_failures + 1
+ failure_msgs.append(e)
+
+ if num_failures > 0:
+ for msg in failure_msgs:
+ print(msg)
+ self.fail("Failed to accept valid commands. %d total failures."
+ "Errors above." % num_failures)
+
+ def test_reject_invalid_commands(self):
+ """
+ For all bad records, attempt to add them and update to them,
+ making sure that both operations fail.
+ """
+ num_failures = 0
+ failure_msgs = []
+
+ # Add invalid records and make sure they fail to be added
+ for dnstype in self.bad_records:
+ for record in self.bad_records[dnstype]:
+ try:
+ result, out, err = self.runsubcmd("dns", "add",
+ os.environ["SERVER"],
+ self.zone, "testrecord",
+ dnstype, record,
+ self.creds_string)
+ self.assertCmdFail(result, "Successfully added invalid"
+ "record '%s' of type '%s'."
+ % (record, dnstype))
+ except AssertionError as e:
+ num_failures = num_failures + 1
+ failure_msgs.append(e)
+ self.resetZone()
+ try:
+ result, out, err = self.runsubcmd("dns", "delete",
+ os.environ["SERVER"],
+ self.zone, "testrecord",
+ dnstype, record,
+ self.creds_string)
+ self.assertCmdFail(result, "Successfully deleted invalid"
+ "record '%s' of type '%s' which"
+ "shouldn't exist." % (record, dnstype))
+ except AssertionError as e:
+ num_failures = num_failures + 1
+ failure_msgs.append(e)
+ self.resetZone()
+
+ # Update valid records to invalid ones and make sure they
+ # fail to be updated
+ for dnstype in self.bad_records:
+ for bad_record in self.bad_records[dnstype]:
+ good_record = self.good_records[dnstype][0]
+
+ try:
+ result, out, err = self.runsubcmd("dns", "add",
+ os.environ["SERVER"],
+ self.zone, "testrecord",
+ dnstype, good_record,
+ self.creds_string)
+ self.assertCmdSuccess(result, out, err, "Failed to add "
+ "record '%s' with type %s."
+ % (record, dnstype))
+
+ result, out, err = self.runsubcmd("dns", "update",
+ os.environ["SERVER"],
+ self.zone, "testrecord",
+ dnstype, good_record,
+ bad_record,
+ self.creds_string)
+ self.assertCmdFail(result, "Successfully updated valid "
+ "record '%s' of type '%s' to invalid "
+ "record '%s' of the same type."
+ % (good_record, dnstype, bad_record))
+
+ result, out, err = self.runsubcmd("dns", "delete",
+ os.environ["SERVER"],
+ self.zone, "testrecord",
+ dnstype, good_record,
+ self.creds_string)
+ self.assertCmdSuccess(result, out, err, "Could not delete "
+ "valid record '%s' of type '%s'."
+ % (good_record, dnstype))
+ except AssertionError as e:
+ num_failures = num_failures + 1
+ failure_msgs.append(e)
+ self.resetZone()
+
+ if num_failures > 0:
+ for msg in failure_msgs:
+ print(msg)
+ self.fail("Failed to reject invalid commands. %d total failures. "
+ "Errors above." % num_failures)
+
+ def test_update_invalid_type(self):
+ """Make sure that a record can't be updated to another type leaving
+ the data the same, where that data would be incompatible with
+ the new type. This is not always enforced at the C level.
+
+ We don't try with all types, because many types are compatible
+ in their representations (e.g. A records could be TXT or CNAME
+ records; PTR record values are exactly the same as CNAME
+ record values, etc).
+ """
+ dnstypes = ('A', 'AAAA', 'SRV')
+ for dnstype1 in dnstypes:
+ record1 = self.good_records[dnstype1][0]
+ result, out, err = self.runsubcmd("dns", "add",
+ os.environ["SERVER"],
+ self.zone, "testrecord",
+ dnstype1, record1,
+ self.creds_string)
+ self.assertCmdSuccess(result, out, err, "Failed to add "
+ "record %s with type %s."
+ % (record1, dnstype1))
+
+ for dnstype2 in dnstypes:
+ if dnstype1 == dnstype2:
+ continue
+
+ record2 = self.good_records[dnstype2][0]
+
+ # Check both ways: Give the current type and try to update,
+ # and give the new type and try to update.
+ result, out, err = self.runsubcmd("dns", "update",
+ os.environ["SERVER"],
+ self.zone, "testrecord",
+ dnstype1, record1,
+ record2, self.creds_string)
+ self.assertCmdFail(result, "Successfully updated record '%s' "
+ "to '%s', even though the latter is of "
+ "type '%s' where '%s' was expected."
+ % (record1, record2, dnstype2, dnstype1))
+
+ result, out, err = self.runsubcmd("dns", "update",
+ os.environ["SERVER"],
+ self.zone, "testrecord",
+ dnstype2, record1, record2,
+ self.creds_string)
+ self.assertCmdFail(result, "Successfully updated record "
+ "'%s' to '%s', even though the former "
+ "is of type '%s' where '%s' was expected."
+ % (record1, record2, dnstype1, dnstype2))
+
+ def test_update_valid_type(self):
+ for dnstype in self.good_records:
+ for record in self.good_records[dnstype]:
+ result, out, err = self.runsubcmd("dns", "add",
+ os.environ["SERVER"],
+ self.zone, "testrecord",
+ dnstype, record,
+ self.creds_string)
+ self.assertCmdSuccess(result, out, err, "Failed to add "
+ "record %s with type %s."
+ % (record, dnstype))
+
+ if record == '.' and dnstype != 'TXT':
+ # This will fail because the update finds a match
+ # for "." that is actually "" (in
+ # dns_record_match()), then uses the "" record in
+ # a call to dns_to_dnsp_convert() which calls
+ # dns_name_check() which rejects "" as a bad DNS
+ # name. Maybe FIXME, maybe not.
+ continue
+
+ # Update the record to be the same.
+ result, out, err = self.runsubcmd("dns", "update",
+ os.environ["SERVER"],
+ self.zone, "testrecord",
+ dnstype, record, record,
+ self.creds_string)
+ self.assertCmdSuccess(result, out, err,
+ "Could not update record "
+ "'%s' to be exactly the same." % record)
+
+ result, out, err = self.runsubcmd("dns", "delete",
+ os.environ["SERVER"],
+ self.zone, "testrecord",
+ dnstype, record,
+ self.creds_string)
+ self.assertCmdSuccess(result, out, err, "Could not delete "
+ "valid record '%s' of type '%s'."
+ % (record, dnstype))
+
+ for record in self.good_records["SRV"]:
+ result, out, err = self.runsubcmd("dns", "add",
+ os.environ["SERVER"],
+ self.zone, "testrecord",
+ "SRV", record,
+ self.creds_string)
+ self.assertCmdSuccess(result, out, err, "Failed to add "
+ "record %s with type 'SRV'." % record)
+
+ split = record.split()
+ new_bit = str(int(split[3]) + 1)
+ new_record = '%s %s %s %s' % (split[0], split[1], split[2], new_bit)
+
+ result, out, err = self.runsubcmd("dns", "update",
+ os.environ["SERVER"],
+ self.zone, "testrecord",
+ "SRV", record,
+ new_record, self.creds_string)
+ self.assertCmdSuccess(result, out, err, "Failed to update record "
+ "'%s' of type '%s' to '%s'."
+ % (record, "SRV", new_record))
+
+ result, out, err = self.runsubcmd("dns", "query",
+ os.environ["SERVER"],
+ self.zone, "testrecord",
+ "SRV", self.creds_string)
+ self.assertCmdSuccess(result, out, err, "Failed to query for "
+ "record '%s' of type '%s'."
+ % (new_record, "SRV"))
+
+ result, out, err = self.runsubcmd("dns", "delete",
+ os.environ["SERVER"],
+ self.zone, "testrecord",
+ "SRV", new_record,
+ self.creds_string)
+ self.assertCmdSuccess(result, out, err, "Could not delete "
+ "valid record '%s' of type '%s'."
+ % (new_record, "SRV"))
+
+ # Since 'dns update' takes the current value as a parameter, make sure
+ # we can't enter the wrong current value for a given record.
+ for dnstype in self.good_records:
+ if len(self.good_records[dnstype]) < 3:
+ continue # Not enough records of this type to do this test
+
+ used_record = self.good_records[dnstype][0]
+ unused_record = self.good_records[dnstype][1]
+ new_record = self.good_records[dnstype][2]
+
+ result, out, err = self.runsubcmd("dns", "add",
+ os.environ["SERVER"],
+ self.zone, "testrecord",
+ dnstype, used_record,
+ self.creds_string)
+ self.assertCmdSuccess(result, out, err, "Failed to add record %s "
+ "with type %s." % (used_record, dnstype))
+
+ result, out, err = self.runsubcmd("dns", "update",
+ os.environ["SERVER"],
+ self.zone, "testrecord",
+ dnstype, unused_record,
+ new_record,
+ self.creds_string)
+ self.assertCmdFail(result, "Successfully updated record '%s' "
+ "from '%s' to '%s', even though the given "
+ "source record is incorrect."
+ % (used_record, unused_record, new_record))
+
+ def test_invalid_types(self):
+ result, out, err = self.runsubcmd("dns", "add",
+ os.environ["SERVER"],
+ self.zone, "testrecord",
+ "SOA", "test",
+ self.creds_string)
+ self.assertCmdFail(result, "Successfully added record of type SOA, "
+ "when this type should not be available.")
+ self.assertTrue("type SOA is not supported" in err,
+ "Invalid error message '%s' when attempting to "
+ "add record of type SOA." % err)
+
+ def test_add_overlapping_different_type(self):
+ """
+ Make sure that we can add an entry with the same name as an existing one but a different type.
+ """
+
+ i = 0
+ for dnstype1 in self.good_records:
+ record1 = self.good_records[dnstype1][0]
+ for dnstype2 in self.good_records:
+ # Only do some subset of dns types, otherwise it takes a long time.
+ i += 1
+ if i % 4 != 0:
+ continue
+
+ if dnstype1 == dnstype2:
+ continue
+
+ record2 = self.good_records[dnstype2][0]
+
+ result, out, err = self.runsubcmd("dns", "add",
+ os.environ["SERVER"],
+ self.zone, "testrecord",
+ dnstype1, record1,
+ self.creds_string)
+ self.assertCmdSuccess(result, out, err, "Failed to add record "
+ "'%s' of type '%s'." % (record1, dnstype1))
+
+ result, out, err = self.runsubcmd("dns", "add",
+ os.environ["SERVER"],
+ self.zone, "testrecord",
+ dnstype2, record2,
+ self.creds_string)
+ self.assertCmdSuccess(result, out, err, "Failed to add record "
+ "'%s' of type '%s' when a record '%s' "
+ "of type '%s' with the same name exists."
+ % (record1, dnstype1, record2, dnstype2))
+
+ result, out, err = self.runsubcmd("dns", "query",
+ os.environ["SERVER"],
+ self.zone, "testrecord",
+ dnstype1, self.creds_string)
+ self.assertCmdSuccess(result, out, err, "Failed to query for "
+ "record '%s' of type '%s' when a new "
+ "record '%s' of type '%s' with the same "
+ "name was added."
+ % (record1, dnstype1, record2, dnstype2))
+
+ result, out, err = self.runsubcmd("dns", "query",
+ os.environ["SERVER"],
+ self.zone, "testrecord",
+ dnstype2, self.creds_string)
+ self.assertCmdSuccess(result, out, err, "Failed to query "
+ "record '%s' of type '%s' which should "
+ "have been added with the same name as "
+ "record '%s' of type '%s'."
+ % (record2, dnstype2, record1, dnstype1))
+
+ result, out, err = self.runsubcmd("dns", "delete",
+ os.environ["SERVER"],
+ self.zone, "testrecord",
+ dnstype1, record1,
+ self.creds_string)
+ self.assertCmdSuccess(result, out, err, "Failed to delete "
+ "record '%s' of type '%s'."
+ % (record1, dnstype1))
+
+ result, out, err = self.runsubcmd("dns", "delete",
+ os.environ["SERVER"],
+ self.zone, "testrecord",
+ dnstype2, record2,
+ self.creds_string)
+ self.assertCmdSuccess(result, out, err, "Failed to delete "
+ "record '%s' of type '%s'."
+ % (record2, dnstype2))
+
+ def test_query_deleted_record(self):
+ self.runsubcmd("dns", "add", os.environ["SERVER"], self.zone,
+ "testrecord", "A", self.testip, self.creds_string)
+ self.runsubcmd("dns", "delete", os.environ["SERVER"], self.zone,
+ "testrecord", "A", self.testip, self.creds_string)
+
+ result, out, err = self.runsubcmd("dns", "query",
+ os.environ["SERVER"],
+ self.zone, "testrecord",
+ "A", self.creds_string)
+ self.assertCmdFail(result)
+
+ def test_add_duplicate_record(self):
+ for record_type in self.good_records:
+ result, out, err = self.runsubcmd("dns", "add",
+ os.environ["SERVER"],
+ self.zone, "testrecord",
+ record_type,
+ self.good_records[record_type][0],
+ self.creds_string)
+ self.assertCmdSuccess(result, out, err)
+ result, out, err = self.runsubcmd("dns", "add",
+ os.environ["SERVER"],
+ self.zone, "testrecord",
+ record_type,
+ self.good_records[record_type][0],
+ self.creds_string)
+ self.assertCmdFail(result)
+ result, out, err = self.runsubcmd("dns", "query",
+ os.environ["SERVER"],
+ self.zone, "testrecord",
+ record_type, self.creds_string)
+ self.assertCmdSuccess(result, out, err)
+ result, out, err = self.runsubcmd("dns", "delete",
+ os.environ["SERVER"],
+ self.zone, "testrecord",
+ record_type,
+ self.good_records[record_type][0],
+ self.creds_string)
+ self.assertCmdSuccess(result, out, err)
+
+ def test_remove_deleted_record(self):
+ self.runsubcmd("dns", "add", os.environ["SERVER"], self.zone,
+ "testrecord", "A", self.testip, self.creds_string)
+ self.runsubcmd("dns", "delete", os.environ["SERVER"], self.zone,
+ "testrecord", "A", self.testip, self.creds_string)
+
+ # Attempting to delete a record that has already been deleted or has never existed should fail
+ result, out, err = self.runsubcmd("dns", "delete",
+ os.environ["SERVER"],
+ self.zone, "testrecord",
+ "A", self.testip, self.creds_string)
+ self.assertCmdFail(result)
+ result, out, err = self.runsubcmd("dns", "query",
+ os.environ["SERVER"],
+ self.zone, "testrecord",
+ "A", self.creds_string)
+ self.assertCmdFail(result)
+ result, out, err = self.runsubcmd("dns", "delete",
+ os.environ["SERVER"],
+ self.zone, "testrecord2",
+ "A", self.testip, self.creds_string)
+ self.assertCmdFail(result)
+
+ def test_cleanup_record(self):
+ """
+ Test dns cleanup command is working fine.
+ """
+
+ # add a A record
+ self.runsubcmd("dns", "add", os.environ["SERVER"], self.zone,
+ 'testa', "A", self.testip, self.creds_string)
+
+ # the above A record points to this host
+ dnshostname = '{0}.{1}'.format('testa', self.zone.lower())
+
+ # add a CNAME record points to above host
+ self.runsubcmd("dns", "add", os.environ["SERVER"], self.zone,
+ 'testcname', "CNAME", dnshostname, self.creds_string)
+
+ # add a NS record
+ self.runsubcmd("dns", "add", os.environ["SERVER"], self.zone,
+ 'testns', "NS", dnshostname, self.creds_string)
+
+ # add a PTR record points to above host
+ self.runsubcmd("dns", "add", os.environ["SERVER"], self.zone,
+ 'testptr', "PTR", dnshostname, self.creds_string)
+
+ # add a SRV record points to above host
+ srv_record = "{0} 65530 65530 65530".format(dnshostname)
+ self.runsubcmd("dns", "add", os.environ["SERVER"], self.zone,
+ 'testsrv', "SRV", srv_record, self.creds_string)
+
+ # cleanup record for this dns host
+ self.runsubcmd("dns", "cleanup", os.environ["SERVER"],
+ dnshostname, self.creds_string)
+
+ # all records should be marked as dNSTombstoned
+ for record_name in ['testa', 'testcname', 'testns', 'testptr', 'testsrv']:
+
+ records = self.samdb.search(
+ base="DC=DomainDnsZones,{0}".format(self.samdb.get_default_basedn()),
+ scope=ldb.SCOPE_SUBTREE,
+ expression="(&(objectClass=dnsNode)(name={0}))".format(record_name),
+ attrs=["dNSTombstoned"])
+
+ self.assertEqual(len(records), 1)
+ for record in records:
+ self.assertEqual(str(record['dNSTombstoned']), 'TRUE')
+
+ def test_cleanup_record_no_A_record(self):
+ """
+ Test dns cleanup command works with no A record.
+ """
+
+ # add a A record
+ self.runsubcmd("dns", "add", os.environ["SERVER"], self.zone,
+ 'notesta', "A", self.testip, self.creds_string)
+
+ # the above A record points to this host
+ dnshostname = '{0}.{1}'.format('testa', self.zone.lower())
+
+ # add a CNAME record points to above host
+ self.runsubcmd("dns", "add", os.environ["SERVER"], self.zone,
+ 'notestcname', "CNAME", dnshostname, self.creds_string)
+
+ # add a NS record
+ self.runsubcmd("dns", "add", os.environ["SERVER"], self.zone,
+ 'notestns', "NS", dnshostname, self.creds_string)
+
+ # add a PTR record points to above host
+ self.runsubcmd("dns", "add", os.environ["SERVER"], self.zone,
+ 'notestptr', "PTR", dnshostname, self.creds_string)
+
+ # add a SRV record points to above host
+ srv_record = "{0} 65530 65530 65530".format(dnshostname)
+ self.runsubcmd("dns", "add", os.environ["SERVER"], self.zone,
+ 'notestsrv', "SRV", srv_record, self.creds_string)
+
+ # Remove the initial A record (leading to hanging references)
+ self.runsubcmd("dns", "delete", os.environ["SERVER"], self.zone,
+ 'notesta', "A", self.testip, self.creds_string)
+
+ # cleanup record for this dns host
+ self.runsubcmd("dns", "cleanup", os.environ["SERVER"],
+ dnshostname, self.creds_string)
+
+ # all records should be marked as dNSTombstoned
+ for record_name in ['notestcname', 'notestns', 'notestptr', 'notestsrv']:
+
+ records = self.samdb.search(
+ base="DC=DomainDnsZones,{0}".format(self.samdb.get_default_basedn()),
+ scope=ldb.SCOPE_SUBTREE,
+ expression="(&(objectClass=dnsNode)(name={0}))".format(record_name),
+ attrs=["dNSTombstoned"])
+
+ self.assertEqual(len(records), 1)
+ for record in records:
+ self.assertEqual(str(record['dNSTombstoned']), 'TRUE')
+
+ def test_cleanup_multi_srv_record(self):
+ """
+ Test dns cleanup command for multi-valued SRV record.
+
+ Steps:
+ - Add 2 A records host1 and host2
+ - Add a SRV record srv1 and points to both host1 and host2
+ - Run cleanup command for host1
+ - Check records for srv1, data for host1 should be gone and host2 is kept.
+ """
+
+ hosts = ['host1', 'host2'] # A record names
+ srv_name = 'srv1'
+
+ # add A records
+ for host in hosts:
+ self.runsubcmd("dns", "add", os.environ["SERVER"], self.zone,
+ host, "A", self.testip, self.creds_string)
+
+ # the above A record points to this host
+ dnshostname = '{0}.{1}'.format(host, self.zone.lower())
+
+ # add a SRV record points to above host
+ srv_record = "{0} 65530 65530 65530".format(dnshostname)
+ self.runsubcmd("dns", "add", os.environ["SERVER"], self.zone,
+ srv_name, "SRV", srv_record, self.creds_string)
+
+ records = self.samdb.search(
+ base="DC=DomainDnsZones,{0}".format(self.samdb.get_default_basedn()),
+ scope=ldb.SCOPE_SUBTREE,
+ expression="(&(objectClass=dnsNode)(name={0}))".format(srv_name),
+ attrs=['dnsRecord'])
+ # should have 2 records here
+ self.assertEqual(len(records[0]['dnsRecord']), 2)
+
+ # cleanup record for dns host1
+ dnshostname1 = 'host1.{0}'.format(self.zone.lower())
+ self.runsubcmd("dns", "cleanup", os.environ["SERVER"],
+ dnshostname1, self.creds_string)
+
+ records = self.samdb.search(
+ base="DC=DomainDnsZones,{0}".format(self.samdb.get_default_basedn()),
+ scope=ldb.SCOPE_SUBTREE,
+ expression="(&(objectClass=dnsNode)(name={0}))".format(srv_name),
+ attrs=['dnsRecord', 'dNSTombstoned'])
+
+ # dnsRecord for host1 should be deleted
+ self.assertEqual(len(records[0]['dnsRecord']), 1)
+
+ # unpack data
+ dns_record_bin = records[0]['dnsRecord'][0]
+ dns_record_obj = ndr_unpack(dnsp.DnssrvRpcRecord, dns_record_bin)
+
+ # dnsRecord for host2 is still there and is the only one
+ dnshostname2 = 'host2.{0}'.format(self.zone.lower())
+ self.assertEqual(dns_record_obj.data.nameTarget, dnshostname2)
+
+ # assert that the record isn't spuriously tombstoned
+ self.assertTrue('dNSTombstoned' not in records[0] or
+ str(records[0]['dNSTombstoned']) == 'FALSE')
+
+ def test_dns_wildcards(self):
+ """
+ Ensure that DNS wild card entries can be added deleted and queried
+ """
+ num_failures = 0
+ failure_msgs = []
+ records = [("*.", "MISS", "A", "1.1.1.1"),
+ ("*.SAMDOM", "MISS.SAMDOM", "A", "1.1.1.2")]
+ for (name, miss, dnstype, record) in records:
+ try:
+ result, out, err = self.runsubcmd("dns", "add",
+ os.environ["SERVER"],
+ self.zone, name,
+ dnstype, record,
+ self.creds_string)
+ self.assertCmdSuccess(
+ result,
+ out,
+ err,
+ ("Failed to add record %s (%s) with type %s."
+ % (name, record, dnstype)))
+
+ result, out, err = self.runsubcmd("dns", "query",
+ os.environ["SERVER"],
+ self.zone, name,
+ dnstype,
+ self.creds_string)
+ self.assertCmdSuccess(
+ result,
+ out,
+ err,
+ ("Failed to query record %s with qualifier %s."
+ % (record, dnstype)))
+
+ # dns tool does not perform dns wildcard search if the name
+ # does not match
+ result, out, err = self.runsubcmd("dns", "query",
+ os.environ["SERVER"],
+ self.zone, miss,
+ dnstype,
+ self.creds_string)
+ self.assertCmdFail(
+ result,
+ ("Failed to query record %s with qualifier %s."
+ % (record, dnstype)))
+
+ result, out, err = self.runsubcmd("dns", "delete",
+ os.environ["SERVER"],
+ self.zone, name,
+ dnstype, record,
+ self.creds_string)
+ self.assertCmdSuccess(
+ result,
+ out,
+ err,
+ ("Failed to remove record %s with type %s."
+ % (record, dnstype)))
+ except AssertionError as e:
+ num_failures = num_failures + 1
+ failure_msgs.append(e)
+
+ if num_failures > 0:
+ for msg in failure_msgs:
+ print(msg)
+ self.fail("Failed to accept valid commands. %d total failures."
+ "Errors above." % num_failures)
+
+ def test_serverinfo(self):
+ for v in ['w2k', 'dotnet', 'longhorn']:
+ result, out, err = self.runsubcmd("dns",
+ "serverinfo",
+ "--client-version", v,
+ os.environ["SERVER"],
+ self.creds_string)
+ self.assertCmdSuccess(result,
+ out,
+ err,
+ "Failed to print serverinfo with "
+ "client version %s" % v)
+ self.assertTrue(out != '')
+
+ def test_zoneinfo(self):
+ result, out, err = self.runsubcmd("dns",
+ "zoneinfo",
+ os.environ["SERVER"],
+ self.zone,
+ self.creds_string)
+ self.assertCmdSuccess(result,
+ out,
+ err,
+ "Failed to print zoneinfo")
+ self.assertTrue(out != '')
+
+ def test_zoneoptions_aging(self):
+ for options, vals, error in (
+ (['--aging=1'], {'fAging': 'TRUE'}, False),
+ (['--aging=0'], {'fAging': 'FALSE'}, False),
+ (['--aging=-1'], {'fAging': 'FALSE'}, True),
+ (['--aging=2'], {}, True),
+ (['--aging=2', '--norefreshinterval=1'], {}, True),
+ (['--aging=1', '--norefreshinterval=1'],
+ {'fAging': 'TRUE', 'dwNoRefreshInterval': '1'}, False),
+ (['--aging=1', '--norefreshinterval=0'],
+ {'fAging': 'TRUE', 'dwNoRefreshInterval': '0'}, False),
+ (['--aging=0', '--norefreshinterval=99', '--refreshinterval=99'],
+ {'fAging': 'FALSE',
+ 'dwNoRefreshInterval': '99',
+ 'dwRefreshInterval': '99'}, False),
+ (['--aging=0', '--norefreshinterval=-99', '--refreshinterval=99'],
+ {}, True),
+ (['--refreshinterval=9999999'], {}, True),
+ (['--norefreshinterval=9999999'], {}, True),
+ ):
+ result, out, err = self.runsubcmd("dns",
+ "zoneoptions",
+ os.environ["SERVER"],
+ self.zone,
+ self.creds_string,
+ *options)
+ if error:
+ self.assertCmdFail(result, "zoneoptions should fail")
+ else:
+ self.assertCmdSuccess(result,
+ out,
+ err,
+ "zoneoptions shouldn't fail")
+
+
+ info_r, info_out, info_err = self.runsubcmd("dns",
+ "zoneinfo",
+ os.environ["SERVER"],
+ self.zone,
+ self.creds_string)
+
+ self.assertCmdSuccess(info_r,
+ info_out,
+ info_err,
+ "zoneinfo shouldn't fail after zoneoptions")
+
+ info = {k: v for k, v in re.findall(r'^\s*(\w+)\s*:\s*(\w+)\s*$',
+ info_out,
+ re.MULTILINE)}
+ for k, v in vals.items():
+ self.assertIn(k, info)
+ self.assertEqual(v, info[k])
+
+
+ def ldap_add_node_with_records(self, name, records):
+ dn = (f"DC={name},DC={self.zone},CN=MicrosoftDNS,DC=DomainDNSZones,"
+ f"{self.samdb.get_default_basedn()}")
+
+ dns_records = []
+ for r in records:
+ rec = dnsp.DnssrvRpcRecord()
+ rec.wType = r.get('wType', dnsp.DNS_TYPE_A)
+ rec.rank = dnsp.DNS_RANK_ZONE
+ rec.dwTtlSeconds = 900
+ rec.dwTimeStamp = r.get('dwTimeStamp', 0)
+ rec.data = r.get('data', '10.10.10.10')
+ dns_records.append(ndr_pack(rec))
+
+ msg = ldb.Message.from_dict(self.samdb,
+ {'dn': dn,
+ "objectClass": ["top", "dnsNode"],
+ 'dnsRecord': dns_records
+ })
+ self.samdb.add(msg)
+
+ def get_timestamp_map(self):
+ re_wtypes = (dnsp.DNS_TYPE_A,
+ dnsp.DNS_TYPE_AAAA,
+ dnsp.DNS_TYPE_TXT)
+
+ t = time.time()
+ now = dsdb_dns.unix_to_dns_timestamp(int(t))
+
+ records = self.get_all_records(self.zone)
+ tsmap = {}
+ for k, recs in records.items():
+ m = []
+ tsmap[k] = m
+ for rec in recs:
+ r = ndr_unpack(dnsp.DnssrvRpcRecord, rec)
+ timestamp = r.dwTimeStamp
+ if abs(timestamp - now) < 3:
+ timestamp = 'nowish'
+
+ if r.wType in re_wtypes:
+ m.append(('R', timestamp))
+ else:
+ m.append(('-', timestamp))
+
+ return tsmap
+
+
+ def test_zoneoptions_mark_records(self):
+ self.maxDiff = 10000
+ # We need a number of records to work with, so we'll use part
+ # of our known good records list, using three different names
+ # to test the regex. All these records will be static.
+ for dnstype in self.good_records:
+ for record in self.good_records[dnstype][:2]:
+ self.runsubcmd("dns", "add",
+ os.environ["SERVER"],
+ self.zone, "frobitz",
+ dnstype, record,
+ self.creds_string)
+ self.runsubcmd("dns", "add",
+ os.environ["SERVER"],
+ self.zone, "weergly",
+ dnstype, record,
+ self.creds_string)
+ self.runsubcmd("dns", "add",
+ os.environ["SERVER"],
+ self.zone, "snizle",
+ dnstype, record,
+ self.creds_string)
+
+ # and we also want some that aren't static, and some mixed
+ # static/dynamic records.
+ # timestamps are in hours since 1601; now ~= 3.7 million
+ for ts in (0, 100, 10 ** 6, 10 ** 7):
+ name = f"ts-{ts}"
+ self.ldap_add_node_with_records(name, [{"dwTimeStamp": ts}])
+
+ recs = []
+ for ts in (0, 100, 10 ** 6, 10 ** 7):
+ addr = f'10.{(ts >> 16) & 255}.{(ts >> 8) & 255}.{ts & 255}'
+ recs.append({"dwTimeStamp": ts, "data": addr})
+
+ self.ldap_add_node_with_records("ts-multi", recs)
+
+ # get the state of ALL records.
+ # then we make assertions about the diffs, keeping track of
+ # the current state.
+
+ tsmap = self.get_timestamp_map()
+
+
+
+ for options, diff, output_substrings, error in (
+ # --mark-old-records-static
+ # --mark-records-static-regex
+ # --mark-records-dynamic-regex
+ (
+ ['--mark-old-records-static=1971-13-04'],
+ {},
+ [],
+ "bad date"
+ ),
+ (
+ # using --dry-run, should be no change, but output.
+ ['--mark-old-records-static=1971-03-04', '--dry-run'],
+ {},
+ [
+ "would make 1/1 records static on ts-1000000.zone.",
+ "would make 1/1 records static on ts-100.zone.",
+ "would make 2/4 records static on ts-multi.zone.",
+ ],
+ False
+ ),
+ (
+ # timestamps < ~ 3.25 million are now static
+ ['--mark-old-records-static=1971-03-04'],
+ {
+ 'ts-100': [('R', 0)],
+ 'ts-1000000': [('R', 0)],
+ 'ts-multi': [('R', 0), ('R', 0), ('R', 0), ('R', 10000000)]
+ },
+ [
+ "made 1/1 records static on ts-1000000.zone.",
+ "made 1/1 records static on ts-100.zone.",
+ "made 2/4 records static on ts-multi.zone.",
+ ],
+ False
+ ),
+ (
+ # no change, old records already static
+ ['--mark-old-records-static=1972-03-04'],
+ {},
+ [],
+ False
+ ),
+ (
+ # no change, samba-tool added records already static
+ ['--mark-records-static-regex=sniz'],
+ {},
+ [],
+ False
+ ),
+ (
+ # snizle has 2 A, 2 AAAA, 10 fancy, and 2 TXT records, in
+ # that order.
+ # the A, AAAA, and TXT records should be dynamic
+ ['--mark-records-dynamic-regex=sniz'],
+ {'snizle': [('R', 'nowish'),
+ ('R', 'nowish'),
+ ('R', 'nowish'),
+ ('R', 'nowish'),
+ ('-', 0),
+ ('-', 0),
+ ('-', 0),
+ ('-', 0),
+ ('-', 0),
+ ('-', 0),
+ ('-', 0),
+ ('-', 0),
+ ('-', 0),
+ ('-', 0),
+ ('R', 'nowish'),
+ ('R', 'nowish')]
+ },
+ ['made 6/16 records dynamic on snizle.zone.'],
+ False
+ ),
+ (
+ # This regex should catch snizle, weergly, and ts-*
+ # but we're doing dry-run so no change
+ ['--mark-records-dynamic-regex=[sw]', '-n'],
+ {},
+ ['would make 3/4 records dynamic on ts-multi.zone.',
+ 'would make 1/1 records dynamic on ts-0.zone.',
+ 'would make 1/1 records dynamic on ts-1000000.zone.',
+ 'would make 6/16 records dynamic on weergly.zone.',
+ 'would make 1/1 records dynamic on ts-100.zone.'
+ ],
+ False
+ ),
+ (
+ # This regex should catch snizle and frobitz
+ # but snizle has already been changed.
+ ['--mark-records-dynamic-regex=z'],
+ {'frobitz': [('R', 'nowish'),
+ ('R', 'nowish'),
+ ('R', 'nowish'),
+ ('R', 'nowish'),
+ ('-', 0),
+ ('-', 0),
+ ('-', 0),
+ ('-', 0),
+ ('-', 0),
+ ('-', 0),
+ ('-', 0),
+ ('-', 0),
+ ('-', 0),
+ ('-', 0),
+ ('R', 'nowish'),
+ ('R', 'nowish')]
+ },
+ ['made 6/16 records dynamic on frobitz.zone.'],
+ False
+ ),
+ (
+ # This regex should catch snizle, frobitz, and
+ # ts-multi. Note that the 1e7 ts-multi record is
+ # already dynamic and doesn't change.
+ ['--mark-records-dynamic-regex=[i]'],
+ {'ts-multi': [('R', 'nowish'),
+ ('R', 'nowish'),
+ ('R', 'nowish'),
+ ('R', 10000000)]
+ },
+ ['made 3/4 records dynamic on ts-multi.zone.'],
+ False
+ ),
+ (
+ # matches no records
+ ['--mark-records-dynamic-regex=^aloooooo[qw]+'],
+ {},
+ [],
+ False
+ ),
+ (
+ # This should be an error, as only one --mark-*
+ # argument is allowed at a time
+ ['--mark-records-dynamic-regex=.',
+ '--mark-records-static-regex=.',
+ ],
+ {},
+ [],
+ True
+ ),
+ (
+ # This should also be an error
+ ['--mark-old-records-static=1997-07-07',
+ '--mark-records-static-regex=.',
+ ],
+ {},
+ [],
+ True
+ ),
+ (
+ # This should not be an error. --aging and refresh
+ # options can be mixed with --mark ones.
+ ['--mark-old-records-static=1997-07-07',
+ '--aging=0',
+ ],
+ {},
+ ['Set Aging to 0'],
+ False
+ ),
+ (
+ # This regex should catch weergly, but all the
+ # records are already static,
+ ['--mark-records-static-regex=wee'],
+ {},
+ [],
+ False
+ ),
+ (
+ # Make frobitz static again.
+ ['--mark-records-static-regex=obi'],
+ {'frobitz': [('R', 0),
+ ('R', 0),
+ ('R', 0),
+ ('R', 0),
+ ('-', 0),
+ ('-', 0),
+ ('-', 0),
+ ('-', 0),
+ ('-', 0),
+ ('-', 0),
+ ('-', 0),
+ ('-', 0),
+ ('-', 0),
+ ('-', 0),
+ ('R', 0),
+ ('R', 0)]
+ },
+ ['made 6/16 records static on frobitz.zone.'],
+ False
+ ),
+ (
+ # would make almost everything static, but --dry-run
+ ['--mark-old-records-static=2222-03-04', '--dry-run'],
+ {},
+ [
+ 'would make 6/16 records static on snizle.zone.',
+ 'would make 3/4 records static on ts-multi.zone.'
+ ],
+ False
+ ),
+ (
+ # make everything static
+ ['--mark-records-static-regex=.'],
+ {'snizle': [('R', 0),
+ ('R', 0),
+ ('R', 0),
+ ('R', 0),
+ ('-', 0),
+ ('-', 0),
+ ('-', 0),
+ ('-', 0),
+ ('-', 0),
+ ('-', 0),
+ ('-', 0),
+ ('-', 0),
+ ('-', 0),
+ ('-', 0),
+ ('R', 0),
+ ('R', 0)],
+ 'ts-10000000': [('R', 0)],
+ 'ts-multi': [('R', 0), ('R', 0), ('R', 0), ('R', 0)]
+ },
+ [
+ 'made 4/4 records static on ts-multi.zone.',
+ 'made 1/1 records static on ts-10000000.zone.',
+ 'made 6/16 records static on snizle.zone.',
+ ],
+ False
+ ),
+ (
+ # make everything dynamic that can be
+ ['--mark-records-dynamic-regex=.'],
+ {'frobitz': [('R', 'nowish'),
+ ('R', 'nowish'),
+ ('R', 'nowish'),
+ ('R', 'nowish'),
+ ('-', 0),
+ ('-', 0),
+ ('-', 0),
+ ('-', 0),
+ ('-', 0),
+ ('-', 0),
+ ('-', 0),
+ ('-', 0),
+ ('-', 0),
+ ('-', 0),
+ ('R', 'nowish'),
+ ('R', 'nowish')],
+ 'snizle': [('R', 'nowish'),
+ ('R', 'nowish'),
+ ('R', 'nowish'),
+ ('R', 'nowish'),
+ ('-', 0),
+ ('-', 0),
+ ('-', 0),
+ ('-', 0),
+ ('-', 0),
+ ('-', 0),
+ ('-', 0),
+ ('-', 0),
+ ('-', 0),
+ ('-', 0),
+ ('R', 'nowish'),
+ ('R', 'nowish')],
+ 'ts-0': [('R', 'nowish')],
+ 'ts-100': [('R', 'nowish')],
+ 'ts-1000000': [('R', 'nowish')],
+ 'ts-10000000': [('R', 'nowish')],
+ 'ts-multi': [('R', 'nowish'),
+ ('R', 'nowish'),
+ ('R', 'nowish'),
+ ('R', 'nowish')],
+ 'weergly': [('R', 'nowish'),
+ ('R', 'nowish'),
+ ('R', 'nowish'),
+ ('R', 'nowish'),
+ ('-', 0),
+ ('-', 0),
+ ('-', 0),
+ ('-', 0),
+ ('-', 0),
+ ('-', 0),
+ ('-', 0),
+ ('-', 0),
+ ('-', 0),
+ ('-', 0),
+ ('R', 'nowish'),
+ ('R', 'nowish')]
+ },
+ [
+ 'made 4/4 records dynamic on ts-multi.zone.',
+ 'made 6/16 records dynamic on snizle.zone.',
+ 'made 1/1 records dynamic on ts-0.zone.',
+ 'made 1/1 records dynamic on ts-1000000.zone.',
+ 'made 1/1 records dynamic on ts-10000000.zone.',
+ 'made 1/1 records dynamic on ts-100.zone.',
+ 'made 6/16 records dynamic on frobitz.zone.',
+ 'made 6/16 records dynamic on weergly.zone.',
+ ],
+ False
+ ),
+ ):
+ result, out, err = self.runsubcmd("dns",
+ "zoneoptions",
+ os.environ["SERVER"],
+ self.zone,
+ self.creds_string,
+ *options)
+ if error:
+ self.assertCmdFail(result, f"zoneoptions should fail ({error})")
+ else:
+ self.assertCmdSuccess(result,
+ out,
+ err,
+ "zoneoptions shouldn't fail")
+
+ new_tsmap = self.get_timestamp_map()
+
+ # same keys, always
+ self.assertEqual(sorted(new_tsmap), sorted(tsmap))
+ changes = {}
+ for k in tsmap:
+ if tsmap[k] != new_tsmap[k]:
+ changes[k] = new_tsmap[k]
+
+ self.assertEqual(diff, changes)
+
+ for s in output_substrings:
+ self.assertIn(s, out)
+ tsmap = new_tsmap
+
+ def test_zonecreate_dns_domain_directory_partition(self):
+ zone = "test-dns-domain-dp-zone"
+ dns_dp_opt = "--dns-directory-partition=domain"
+
+ result, out, err = self.runsubcmd("dns",
+ "zonecreate",
+ os.environ["SERVER"],
+ zone,
+ self.creds_string,
+ dns_dp_opt)
+ self.assertCmdSuccess(result,
+ out,
+ err,
+ "Failed to create zone with "
+ "--dns-directory-partition option")
+ self.assertTrue('Zone %s created successfully' % zone in out,
+ "Unexpected output: %s")
+
+ result, out, err = self.runsubcmd("dns",
+ "zoneinfo",
+ os.environ["SERVER"],
+ zone,
+ self.creds_string)
+ self.assertCmdSuccess(result, out, err)
+ self.assertTrue("DNS_DP_DOMAIN_DEFAULT" in out,
+ "Missing DNS_DP_DOMAIN_DEFAULT flag")
+
+ result, out, err = self.runsubcmd("dns",
+ "zonedelete",
+ os.environ["SERVER"],
+ zone,
+ self.creds_string)
+ self.assertCmdSuccess(result, out, err,
+ "Failed to delete zone in domain DNS directory "
+ "partition")
+ result, out, err = self.runsubcmd("dns",
+ "zonelist",
+ os.environ["SERVER"],
+ self.creds_string)
+ self.assertCmdSuccess(result, out, err,
+ "Failed to delete zone in domain DNS directory "
+ "partition")
+ self.assertTrue(zone not in out,
+ "Deleted zone still exists")
+
+ def test_zonecreate_dns_forest_directory_partition(self):
+ zone = "test-dns-forest-dp-zone"
+ dns_dp_opt = "--dns-directory-partition=forest"
+
+ result, out, err = self.runsubcmd("dns",
+ "zonecreate",
+ os.environ["SERVER"],
+ zone,
+ self.creds_string,
+ dns_dp_opt)
+ self.assertCmdSuccess(result,
+ out,
+ err,
+ "Failed to create zone with "
+ "--dns-directory-partition option")
+ self.assertTrue('Zone %s created successfully' % zone in out,
+ "Unexpected output: %s")
+
+ result, out, err = self.runsubcmd("dns",
+ "zoneinfo",
+ os.environ["SERVER"],
+ zone,
+ self.creds_string)
+ self.assertCmdSuccess(result, out, err)
+ self.assertTrue("DNS_DP_FOREST_DEFAULT" in out,
+ "Missing DNS_DP_FOREST_DEFAULT flag")
+
+ result, out, err = self.runsubcmd("dns",
+ "zonedelete",
+ os.environ["SERVER"],
+ zone,
+ self.creds_string)
+ self.assertCmdSuccess(result, out, err,
+ "Failed to delete zone in forest DNS directory "
+ "partition")
+
+ result, out, err = self.runsubcmd("dns",
+ "zonelist",
+ os.environ["SERVER"],
+ self.creds_string)
+ self.assertCmdSuccess(result, out, err,
+ "Failed to delete zone in forest DNS directory "
+ "partition")
+ self.assertTrue(zone not in out,
+ "Deleted zone still exists")
diff --git a/python/samba/tests/samba_tool/domain_auth_policy.py b/python/samba/tests/samba_tool/domain_auth_policy.py
new file mode 100644
index 0000000..1854037
--- /dev/null
+++ b/python/samba/tests/samba_tool/domain_auth_policy.py
@@ -0,0 +1,1517 @@
+# Unix SMB/CIFS implementation.
+#
+# Tests for samba-tool domain auth policy command
+#
+# Copyright (C) Catalyst.Net Ltd. 2023
+#
+# Written by Rob van der Linde <rob@catalyst.net.nz>
+#
+# 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 json
+from optparse import OptionValueError
+from unittest.mock import patch
+
+from samba.dcerpc import security
+from samba.ndr import ndr_pack, ndr_unpack
+from samba.netcmd.domain.models.exceptions import ModelError
+from samba.samdb import SamDB
+from samba.sd_utils import SDUtils
+
+from .silo_base import SiloTest
+
+
+class AuthPolicyCmdTestCase(SiloTest):
+
+ def test_list(self):
+ """Test listing authentication policies in list format."""
+ result, out, err = self.runcmd("domain", "auth", "policy", "list")
+ self.assertIsNone(result, msg=err)
+
+ expected_policies = ["User Policy", "Service Policy", "Computer Policy"]
+
+ for policy in expected_policies:
+ self.assertIn(policy, out)
+
+ def test_list__json(self):
+ """Test listing authentication policies in JSON format."""
+ result, out, err = self.runcmd("domain", "auth", "policy",
+ "list", "--json")
+ self.assertIsNone(result, msg=err)
+
+ # we should get valid json
+ policies = json.loads(out)
+
+ expected_policies = ["User Policy", "Service Policy", "Computer Policy"]
+
+ for name in expected_policies:
+ policy = policies[name]
+ self.assertIn("name", policy)
+ self.assertIn("msDS-AuthNPolicy", list(policy["objectClass"]))
+ self.assertIn("msDS-AuthNPolicyEnforced", policy)
+ self.assertIn("msDS-StrongNTLMPolicy", policy)
+ self.assertIn("objectGUID", policy)
+
+ def test_view(self):
+ """Test viewing a single authentication policy."""
+ result, out, err = self.runcmd("domain", "auth", "policy", "view",
+ "--name", "User Policy")
+ self.assertIsNone(result, msg=err)
+
+ # we should get valid json
+ policy = json.loads(out)
+
+ # check a few fields only
+ self.assertEqual(policy["cn"], "User Policy")
+ self.assertEqual(policy["msDS-AuthNPolicyEnforced"], True)
+
+ def test_view__notfound(self):
+ """Test viewing an authentication policy that doesn't exist."""
+ result, out, err = self.runcmd("domain", "auth", "policy", "view",
+ "--name", "doesNotExist")
+ self.assertEqual(result, -1)
+ self.assertIn("Authentication policy doesNotExist not found.", err)
+
+ def test_view__name_required(self):
+ """Test view authentication policy without --name argument."""
+ result, out, err = self.runcmd("domain", "auth", "policy", "view")
+ self.assertEqual(result, -1)
+ self.assertIn("Argument --name is required.", err)
+
+ def test_create__success(self):
+ """Test creating a new authentication policy."""
+ name = self.unique_name()
+
+ self.addCleanup(self.delete_authentication_policy, name=name, force=True)
+ result, out, err = self.runcmd("domain", "auth", "policy", "create",
+ "--name", name)
+ self.assertIsNone(result, msg=err)
+
+ # Check policy that was created
+ policy = self.get_authentication_policy(name)
+ self.assertEqual(str(policy["cn"]), name)
+ self.assertEqual(str(policy["msDS-AuthNPolicyEnforced"]), "TRUE")
+
+ def test_create__description(self):
+ """Test creating a new authentication policy with description set."""
+ name = self.unique_name()
+
+ self.addCleanup(self.delete_authentication_policy, name=name, force=True)
+ result, out, err = self.runcmd("domain", "auth", "policy", "create",
+ "--name", name,
+ "--description", "Custom Description")
+ self.assertIsNone(result, msg=err)
+
+ # Check policy description
+ policy = self.get_authentication_policy(name)
+ self.assertEqual(str(policy["cn"]), name)
+ self.assertEqual(str(policy["description"]), "Custom Description")
+
+ def test_create__user_tgt_lifetime_mins(self):
+ """Test create a new authentication policy with --user-tgt-lifetime-mins.
+
+ Also checks the upper and lower bounds are handled.
+ """
+ name = self.unique_name()
+
+ self.addCleanup(self.delete_authentication_policy, name=name, force=True)
+ result, out, err = self.runcmd("domain", "auth", "policy", "create",
+ "--name", name,
+ "--user-tgt-lifetime-mins", "60")
+ self.assertIsNone(result, msg=err)
+
+ # Check policy fields.
+ policy = self.get_authentication_policy(name)
+ self.assertEqual(str(policy["cn"]), name)
+ self.assertEqual(str(policy["msDS-UserTGTLifetime"]), "60")
+
+ # check lower bounds (45)
+ result, out, err = self.runcmd("domain", "auth", "policy", "create",
+ "--name", name + "Lower",
+ "--user-tgt-lifetime-mins", "44")
+ self.assertEqual(result, -1)
+ self.assertIn("--user-tgt-lifetime-mins must be between 45 and 2147483647",
+ err)
+
+ # check upper bounds (2147483647)
+ result, out, err = self.runcmd("domain", "auth", "policy", "create",
+ "--name", name + "Upper",
+ "--user-tgt-lifetime-mins", "2147483648")
+ self.assertEqual(result, -1)
+ self.assertIn("--user-tgt-lifetime-mins must be between 45 and 2147483647",
+ err)
+
+ def test_create__user_allowed_to_authenticate_from_device_group(self):
+ """Tests the --user-allowed-to-authenticate-from-device-group shortcut."""
+ name = self.unique_name()
+ expected = "O:SYG:SYD:(XA;OICI;CR;;;WD;(Member_of_any {SID(%s)}))" % (
+ self.device_group.object_sid)
+
+ self.addCleanup(self.delete_authentication_policy, name=name, force=True)
+ result, out, err = self.runcmd("domain", "auth", "policy", "create",
+ "--name", name,
+ "--user-allowed-to-authenticate-from-device-group",
+ self.device_group.name)
+ self.assertIsNone(result, msg=err)
+
+ # Check policy fields.
+ policy = self.get_authentication_policy(name)
+ self.assertEqual(str(policy["cn"]), name)
+
+ # Check generated SDDL.
+ desc = policy["msDS-UserAllowedToAuthenticateFrom"][0]
+ sddl = ndr_unpack(security.descriptor, desc).as_sddl()
+ self.assertEqual(sddl, expected)
+
+ def test_create__user_allowed_to_authenticate_from_device_silo(self):
+ """Tests the --user-allowed-to-authenticate-from-device-silo shortcut."""
+ name = self.unique_name()
+
+ self.addCleanup(self.delete_authentication_policy, name=name, force=True)
+ result, out, err = self.runcmd("domain", "auth", "policy", "create",
+ "--name", name,
+ "--user-allowed-to-authenticate-from-device-silo",
+ "Developers")
+ self.assertIsNone(result, msg=err)
+
+ # Check policy fields.
+ policy = self.get_authentication_policy(name)
+ self.assertEqual(str(policy["cn"]), name)
+
+ # Check generated SDDL.
+ desc = policy["msDS-UserAllowedToAuthenticateFrom"][0]
+ sddl = ndr_unpack(security.descriptor, desc).as_sddl()
+ self.assertEqual(
+ sddl,
+ 'O:SYG:SYD:(XA;OICI;CR;;;WD;(@USER.ad://ext/AuthenticationSilo == "Developers"))')
+
+ def test_create__user_allowed_to_authenticate_to_by_group(self):
+ """Tests the --user-allowed-to-authenticate-to-by-group shortcut."""
+ name = self.unique_name()
+ expected = "O:SYG:SYD:(XA;OICI;CR;;;WD;(Member_of_any {SID(%s)}))" % (
+ self.device_group.object_sid)
+
+ # Create a user with authenticate to by group attribute.
+ self.addCleanup(self.delete_authentication_policy, name=name, force=True)
+ result, out, err = self.runcmd(
+ "domain", "auth", "policy", "create", "--name", name,
+ "--user-allowed-to-authenticate-to-by-group",
+ self.device_group.name)
+ self.assertIsNone(result, msg=err)
+
+ # Check user allowed to authenticate to field was modified.
+ policy = self.get_authentication_policy(name)
+ self.assertEqual(str(policy["cn"]), name)
+ desc = policy["msDS-UserAllowedToAuthenticateTo"][0]
+ sddl = ndr_unpack(security.descriptor, desc).as_sddl()
+ self.assertEqual(sddl, expected)
+
+ def test_create__user_allowed_to_authenticate_to_by_silo(self):
+ """Tests the --user-allowed-to-authenticate-to-by-silo shortcut."""
+ name = self.unique_name()
+ expected = ('O:SYG:SYD:(XA;OICI;CR;;;WD;(@USER.ad://ext/'
+ 'AuthenticationSilo == "QA"))')
+
+ # Create a user with authenticate to by silo attribute.
+ self.addCleanup(self.delete_authentication_policy, name=name, force=True)
+ result, out, err = self.runcmd(
+ "domain", "auth", "policy", "create", "--name", name,
+ "--user-allowed-to-authenticate-to-by-silo", "QA")
+ self.assertIsNone(result, msg=err)
+
+ # Check user allowed to authenticate to field was modified.
+ policy = self.get_authentication_policy(name)
+ self.assertEqual(str(policy["cn"]), name)
+ desc = policy["msDS-UserAllowedToAuthenticateTo"][0]
+ sddl = ndr_unpack(security.descriptor, desc).as_sddl()
+ self.assertEqual(sddl, expected)
+
+ def test_create__service_tgt_lifetime_mins(self):
+ """Test create a new authentication policy with --service-tgt-lifetime-mins.
+
+ Also checks the upper and lower bounds are handled.
+ """
+ name = self.unique_name()
+
+ self.addCleanup(self.delete_authentication_policy, name=name, force=True)
+ result, out, err = self.runcmd("domain", "auth", "policy", "create",
+ "--name", name,
+ "--service-tgt-lifetime-mins", "60")
+ self.assertIsNone(result, msg=err)
+
+ # Check policy fields.
+ policy = self.get_authentication_policy(name)
+ self.assertEqual(str(policy["cn"]), name)
+ self.assertEqual(str(policy["msDS-ServiceTGTLifetime"]), "60")
+
+ # check lower bounds (45)
+ result, out, err = self.runcmd("domain", "auth", "policy", "create",
+ "--name", name,
+ "--service-tgt-lifetime-mins", "44")
+ self.assertEqual(result, -1)
+ self.assertIn("--service-tgt-lifetime-mins must be between 45 and 2147483647",
+ err)
+
+ # check upper bounds (2147483647)
+ result, out, err = self.runcmd("domain", "auth", "policy", "create",
+ "--name", name,
+ "--service-tgt-lifetime-mins", "2147483648")
+ self.assertEqual(result, -1)
+ self.assertIn("--service-tgt-lifetime-mins must be between 45 and 2147483647",
+ err)
+
+ def test_create__service_allowed_to_authenticate_from_device_group(self):
+ """Tests the --service-allowed-to-authenticate-from-device-group shortcut."""
+ name = self.unique_name()
+ expected = "O:SYG:SYD:(XA;OICI;CR;;;WD;(Member_of_any {SID(%s)}))" % (
+ self.device_group.object_sid)
+
+ self.addCleanup(self.delete_authentication_policy, name=name, force=True)
+ result, out, err = self.runcmd("domain", "auth", "policy", "create",
+ "--name", name,
+ "--service-allowed-to-authenticate-from-device-group",
+ self.device_group.name)
+ self.assertIsNone(result, msg=err)
+
+ # Check policy fields.
+ policy = self.get_authentication_policy(name)
+ self.assertEqual(str(policy["cn"]), name)
+
+ # Check generated SDDL.
+ desc = policy["msDS-ServiceAllowedToAuthenticateFrom"][0]
+ sddl = ndr_unpack(security.descriptor, desc).as_sddl()
+ self.assertEqual(sddl, expected)
+
+ def test_create__service_allowed_to_authenticate_from_device_silo(self):
+ """Tests the --service-allowed-to-authenticate-from-device-silo shortcut."""
+ name = self.unique_name()
+
+ self.addCleanup(self.delete_authentication_policy, name=name, force=True)
+ result, out, err = self.runcmd("domain", "auth", "policy", "create",
+ "--name", name,
+ "--service-allowed-to-authenticate-from-device-silo",
+ "Managers")
+ self.assertIsNone(result, msg=err)
+
+ # Check policy fields.
+ policy = self.get_authentication_policy(name)
+ self.assertEqual(str(policy["cn"]), name)
+ desc = policy["msDS-ServiceAllowedToAuthenticateFrom"][0]
+
+ # Check generated SDDL.
+ sddl = ndr_unpack(security.descriptor, desc).as_sddl()
+ self.assertEqual(
+ sddl,
+ 'O:SYG:SYD:(XA;OICI;CR;;;WD;(@USER.ad://ext/AuthenticationSilo == "Managers"))')
+
+ def test_create__service_allowed_to_authenticate_to_by_group(self):
+ """Tests the --service-allowed-to-authenticate-to-by-group shortcut."""
+ name = self.unique_name()
+ expected = "O:SYG:SYD:(XA;OICI;CR;;;WD;(Member_of_any {SID(%s)}))" % (
+ self.device_group.object_sid)
+
+ # Create a user with authenticate to by group attribute.
+ self.addCleanup(self.delete_authentication_policy, name=name, force=True)
+ result, out, err = self.runcmd(
+ "domain", "auth", "policy", "create", "--name", name,
+ "--service-allowed-to-authenticate-to-by-group",
+ self.device_group.name)
+ self.assertIsNone(result, msg=err)
+
+ # Check user allowed to authenticate to field was modified.
+ policy = self.get_authentication_policy(name)
+ self.assertEqual(str(policy["cn"]), name)
+ desc = policy["msDS-ServiceAllowedToAuthenticateTo"][0]
+ sddl = ndr_unpack(security.descriptor, desc).as_sddl()
+ self.assertEqual(sddl, expected)
+
+ def test_create__service_allowed_to_authenticate_to_by_silo(self):
+ """Tests the --service-allowed-to-authenticate-to-by-silo shortcut."""
+ name = self.unique_name()
+ expected = ('O:SYG:SYD:(XA;OICI;CR;;;WD;(@USER.ad://ext/'
+ 'AuthenticationSilo == "Managers"))')
+
+ # Create a user with authenticate to by silo attribute.
+ self.addCleanup(self.delete_authentication_policy, name=name, force=True)
+ result, out, err = self.runcmd(
+ "domain", "auth", "policy", "create", "--name", name,
+ "--service-allowed-to-authenticate-to-by-silo", "Managers")
+ self.assertIsNone(result, msg=err)
+
+ # Check user allowed to authenticate to field was modified.
+ policy = self.get_authentication_policy(name)
+ self.assertEqual(str(policy["cn"]), name)
+ desc = policy["msDS-ServiceAllowedToAuthenticateTo"][0]
+ sddl = ndr_unpack(security.descriptor, desc).as_sddl()
+ self.assertEqual(sddl, expected)
+
+ def test_create__computer_tgt_lifetime_mins(self):
+ """Test create a new authentication policy with --computer-tgt-lifetime-mins.
+
+ Also checks the upper and lower bounds are handled.
+ """
+ name = self.unique_name()
+
+ self.addCleanup(self.delete_authentication_policy, name=name, force=True)
+ result, out, err = self.runcmd("domain", "auth", "policy", "create",
+ "--name", name,
+ "--computer-tgt-lifetime-mins", "60")
+ self.assertIsNone(result, msg=err)
+
+ # Check policy fields.
+ policy = self.get_authentication_policy(name)
+ self.assertEqual(str(policy["cn"]), name)
+ self.assertEqual(str(policy["msDS-ComputerTGTLifetime"]), "60")
+
+ # check lower bounds (45)
+ result, out, err = self.runcmd("domain", "auth", "policy", "create",
+ "--name", name + "Lower",
+ "--computer-tgt-lifetime-mins", "44")
+ self.assertEqual(result, -1)
+ self.assertIn("--computer-tgt-lifetime-mins must be between 45 and 2147483647",
+ err)
+
+ # check upper bounds (2147483647)
+ result, out, err = self.runcmd("domain", "auth", "policy", "create",
+ "--name", name + "Upper",
+ "--computer-tgt-lifetime-mins", "2147483648")
+ self.assertEqual(result, -1)
+ self.assertIn("--computer-tgt-lifetime-mins must be between 45 and 2147483647",
+ err)
+
+ def test_create__computer_allowed_to_authenticate_to_by_group(self):
+ """Tests the --computer-allowed-to-authenticate-to-by-group shortcut."""
+ name = self.unique_name()
+ expected = "O:SYG:SYD:(XA;OICI;CR;;;WD;(Member_of_any {SID(%s)}))" % (
+ self.device_group.object_sid)
+
+ # Create a user with authenticate to by group attribute.
+ self.addCleanup(self.delete_authentication_policy, name=name, force=True)
+ result, out, err = self.runcmd(
+ "domain", "auth", "policy", "create", "--name", name,
+ "--computer-allowed-to-authenticate-to-by-group",
+ self.device_group.name)
+ self.assertIsNone(result, msg=err)
+
+ # Check user allowed to authenticate to field was modified.
+ policy = self.get_authentication_policy(name)
+ self.assertEqual(str(policy["cn"]), name)
+ desc = policy["msDS-ComputerAllowedToAuthenticateTo"][0]
+ sddl = ndr_unpack(security.descriptor, desc).as_sddl()
+ self.assertEqual(sddl, expected)
+
+ def test_create__computer_allowed_to_authenticate_to_by_silo(self):
+ """Tests the --computer-allowed-to-authenticate-to-by-silo shortcut."""
+ name = self.unique_name()
+ expected = ('O:SYG:SYD:(XA;OICI;CR;;;WD;(@USER.ad://ext/'
+ 'AuthenticationSilo == "QA"))')
+
+ # Create a user with authenticate to by silo attribute.
+ self.addCleanup(self.delete_authentication_policy, name=name, force=True)
+ result, out, err = self.runcmd(
+ "domain", "auth", "policy", "create", "--name", name,
+ "--computer-allowed-to-authenticate-to-by-silo", "QA")
+ self.assertIsNone(result, msg=err)
+
+ # Check user allowed to authenticate to field was modified.
+ policy = self.get_authentication_policy(name)
+ self.assertEqual(str(policy["cn"]), name)
+ desc = policy["msDS-ComputerAllowedToAuthenticateTo"][0]
+ sddl = ndr_unpack(security.descriptor, desc).as_sddl()
+ self.assertEqual(sddl, expected)
+
+ def test_create__valid_sddl(self):
+ """Test creating a new authentication policy with valid SDDL in a field."""
+ name = self.unique_name()
+ expected = "O:SYG:SYD:(XA;OICI;CR;;;WD;(Member_of {SID(AO)}))"
+
+ self.addCleanup(self.delete_authentication_policy, name=name, force=True)
+ result, out, err = self.runcmd("domain", "auth", "policy", "create",
+ "--name", name,
+ "--user-allowed-to-authenticate-from",
+ expected)
+ self.assertIsNone(result, msg=err)
+
+ # Check policy fields.
+ policy = self.get_authentication_policy(name)
+ self.assertEqual(str(policy["cn"]), name)
+ desc = policy["msDS-UserAllowedToAuthenticateFrom"][0]
+ sddl = ndr_unpack(security.descriptor, desc).as_sddl()
+ self.assertEqual(sddl, expected)
+
+ def test_create__invalid_sddl(self):
+ """Test creating a new authentication policy with invalid SDDL in a field."""
+ name = self.unique_name()
+
+ result, out, err = self.runcmd("domain", "auth", "policy", "create",
+ "--name", name,
+ "--user-allowed-to-authenticate-from",
+ "*INVALID SDDL*")
+
+ self.assertEqual(result, -1)
+ self.assertIn("Unable to parse SDDL", err)
+ self.assertIn(" *INVALID SDDL*\n ^\n expected '[OGDS]:' section start ", err)
+
+ def test_create__invalid_sddl_conditional_ace(self):
+ """Test creating a new authentication policy with invalid SDDL in a field."""
+ sddl = "O:SYG:SYD:(XA;OICI;CR;;;WD;(Member_of {secret club}))"
+ result, out, err = self.runcmd("domain", "auth", "policy", "create",
+ "--name", "invalidSDDLPolicy2",
+ "--user-allowed-to-authenticate-from",
+ sddl)
+ self.assertEqual(result, -1)
+ self.assertIn("Unable to parse SDDL", err)
+ self.assertIn(sddl, err)
+ self.assertIn(f"\n{'^':>41}", err)
+ self.assertIn("unexpected byte 0x73 's' parsing literal", err)
+ self.assertNotIn(" File ", err)
+
+ def test_create__invalid_sddl_conditional_ace_non_ascii(self):
+ """Test creating a new authentication policy with invalid SDDL in a field."""
+ sddl = 'O:SYG:SYD:(XA;OICI;CR;;;WD;(@User.āāēē == "łē¶ŧ¹⅓þōīŋ“đ¢ð»" && Member_of {secret club}))'
+ result, out, err = self.runcmd("domain", "auth", "policy", "create",
+ "--name", "invalidSDDLPolicy2",
+ "--user-allowed-to-authenticate-from",
+ sddl)
+ self.assertEqual(result, -1)
+ self.assertIn("Unable to parse SDDL", err)
+ self.assertIn(sddl, err)
+ self.assertIn(f"\n{'^':>76}\n", err)
+ self.assertIn(" unexpected byte 0x73 's' parsing literal", err)
+ self.assertNotIn(" File ", err)
+
+ def test_create__invalid_sddl_normal_ace(self):
+ """Test creating a new authentication policy with invalid SDDL in a field."""
+ sddl = "O:SYG:SYD:(A;;;;ZZ)(XA;OICI;CR;;;WD;(Member_of {WD}))"
+ result, out, err = self.runcmd("domain", "auth", "policy", "create",
+ "--name", "invalidSDDLPolicy3",
+ "--user-allowed-to-authenticate-from",
+ sddl)
+ self.assertEqual(result, -1)
+ self.assertIn("Unable to parse SDDL", err)
+ self.assertIn(sddl, err)
+ self.assertIn(f"\n{'^':>13}", err)
+ self.assertIn("\n malformed ACE with only 4 ';'\n", err)
+ self.assertNotIn(" File ", err) # traceback marker
+
+ def test_create__device_attribute_in_sddl_allowed_to(self):
+ """Test creating a new authentication policy that uses
+ user-allowed-to-authenticate-to with a device attribute."""
+
+ sddl = 'O:SYG:SYD:(XA;OICI;CR;;;WD;(@Device.claim == "foo"))'
+
+ name = self.unique_name()
+ self.addCleanup(self.delete_authentication_policy, name=name)
+ result, _, err = self.runcmd("domain", "auth", "policy", "create",
+ "--name", name,
+ "--user-allowed-to-authenticate-to",
+ sddl)
+ self.assertIsNone(result, msg=err)
+
+ def test_create__device_operator_in_sddl_allowed_to(self):
+ """Test creating a new authentication policy that uses
+ user-allowed-to-authenticate-to with a device operator."""
+
+ sddl = 'O:SYG:SYD:(XA;OICI;CR;;;WD;(Not_Device_Member_of {SID(WD)}))'
+
+ name = self.unique_name()
+ self.addCleanup(self.delete_authentication_policy, name=name)
+ result, _, err = self.runcmd("domain", "auth", "policy", "create",
+ "--name", name,
+ "--user-allowed-to-authenticate-to",
+ sddl)
+ self.assertIsNone(result, msg=err)
+
+ def test_create__device_attribute_in_sddl_allowed_from(self):
+ """Test creating a new authentication policy that uses
+ user-allowed-to-authenticate-from with a device attribute."""
+
+ sddl = 'O:SYG:SYD:(XA;OICI;CR;;;WD;(@Device.claim == "foo"))'
+
+ name = self.unique_name()
+ result, _, err = self.runcmd("domain", "auth", "policy", "create",
+ "--name", name,
+ "--user-allowed-to-authenticate-from",
+ sddl)
+ self.assertEqual(result, -1)
+ self.assertIn("Unable to parse SDDL", err)
+ self.assertIn(sddl, err)
+ self.assertIn(f"\n{'^':>31}\n", err)
+ self.assertIn(" a device attribute is not applicable in this context "
+ "(did you intend a user attribute?)",
+ err)
+ self.assertNotIn(" File ", err)
+
+ def test_create__device_operator_in_sddl_allowed_from(self):
+ """Test creating a new authentication policy that uses
+ user-allowed-to-authenticate-from with a device operator."""
+
+ sddl = 'O:SYG:SYD:(XA;OICI;CR;;;WD;(Not_Device_Member_of {SID(WD)}))'
+
+ name = self.unique_name()
+ result, _, err = self.runcmd("domain", "auth", "policy", "create",
+ "--name", name,
+ "--user-allowed-to-authenticate-from",
+ sddl)
+ self.assertEqual(result, -1)
+ self.assertIn("Unable to parse SDDL", err)
+ self.assertIn(sddl, err)
+ self.assertIn(f"\n{'^':>30}\n", err)
+ self.assertIn(" a device‐relative expression will never evaluate to "
+ "true in this context (did you intend a user‐relative "
+ "expression?)",
+ err)
+ self.assertNotIn(" File ", err)
+
+ def test_create__device_attribute_in_sddl_already_exists(self):
+ """Test modifying an existing authentication policy that uses
+ user-allowed-to-authenticate-from with a device attribute."""
+
+ # The SDDL refers to ‘Device.claim’.
+ sddl = 'O:SYG:SYD:(XA;OICI;CR;;;WD;(@Device.claim == "foo"))'
+ domain_sid = security.dom_sid(self.samdb.get_domain_sid())
+ descriptor = security.descriptor.from_sddl(sddl, domain_sid)
+
+ # Manually create an authentication policy that refers to a device
+ # attribute.
+
+ name = self.unique_name()
+ dn = self.get_authn_policies_dn()
+ dn.add_child(f"CN={name}")
+ message = {
+ 'dn': dn,
+ 'msDS-AuthNPolicyEnforced': b'TRUE',
+ 'objectClass': b'msDS-AuthNPolicy',
+ 'msDS-UserAllowedToAuthenticateFrom': ndr_pack(descriptor),
+ }
+
+ self.addCleanup(self.delete_authentication_policy, name=name)
+ self.samdb.add(message)
+
+ # Change the policy description. This should succeed, in spite of the
+ # policy’s referring to a device attribute when it shouldn’t.
+ result, _, err = self.runcmd("domain", "auth", "policy", "modify",
+ "--name", name,
+ "--description", "NewDescription")
+ self.assertIsNone(result, msg=err)
+
+ def test_create__already_exists(self):
+ """Test creating a new authentication policy that already exists."""
+ result, out, err = self.runcmd("domain", "auth", "policy", "create",
+ "--name", "User Policy")
+ self.assertEqual(result, -1)
+ self.assertIn("Authentication policy User Policy already exists", err)
+
+ def test_create__name_missing(self):
+ """Test create authentication policy without --name argument."""
+ result, out, err = self.runcmd("domain", "auth", "policy", "create")
+ self.assertEqual(result, -1)
+ self.assertIn("Argument --name is required.", err)
+
+ def test_create__audit(self):
+ """Test create authentication policy with --audit flag."""
+ name = self.unique_name()
+
+ self.addCleanup(self.delete_authentication_policy, name=name, force=True)
+ result, out, err = self.runcmd("domain", "auth", "policy", "create",
+ "--name", name,
+ "--audit")
+ self.assertIsNone(result, msg=err)
+
+ # fetch and check policy
+ policy = self.get_authentication_policy(name)
+ self.assertEqual(str(policy["msDS-AuthNPolicyEnforced"]), "FALSE")
+
+ def test_create__enforce(self):
+ """Test create authentication policy with --enforce flag."""
+ name = self.unique_name()
+
+ self.addCleanup(self.delete_authentication_policy, name=name, force=True)
+ result, out, err = self.runcmd("domain", "auth", "policy", "create",
+ "--name", name,
+ "--enforce")
+ self.assertIsNone(result, msg=err)
+
+ # fetch and check policy
+ policy = self.get_authentication_policy(name)
+ self.assertEqual(str(policy["msDS-AuthNPolicyEnforced"]), "TRUE")
+
+ def test_create__audit_enforce_together(self):
+ """Test create auth policy using both --audit and --enforce."""
+ name = self.unique_name()
+
+ result, out, err = self.runcmd("domain", "auth", "policy", "create",
+ "--name", name,
+ "--audit", "--enforce")
+
+ self.assertEqual(result, -1)
+ self.assertIn("--audit and --enforce cannot be used together.", err)
+
+ def test_create__protect_unprotect_together(self):
+ """Test create authentication policy using --protect and --unprotect."""
+ name = self.unique_name()
+
+ result, out, err = self.runcmd("domain", "auth", "policy", "create",
+ "--name", name,
+ "--protect", "--unprotect")
+
+ self.assertEqual(result, -1)
+ self.assertIn("--protect and --unprotect cannot be used together.", err)
+
+ def test_create__user_allowed_to_authenticate_from_repeated(self):
+ """Test repeating similar arguments doesn't make sense to use together.
+
+ --user-allowed-to-authenticate-from
+ --user-allowed-to-authenticate-from-device-silo
+ """
+ sddl = 'O:SYG:SYD:(XA;OICI;CR;;;WD;(@USER.ad://ext/AuthenticationSilo == "Developers"))'
+ name = self.unique_name()
+
+ result, out, err = self.runcmd("domain", "auth", "policy", "create",
+ "--name", name,
+ "--user-allowed-to-authenticate-from",
+ sddl,
+ "--user-allowed-to-authenticate-from-device-silo",
+ "Managers")
+
+ self.assertEqual(result, -1)
+ self.assertIn("--user-allowed-to-authenticate-from argument repeated 2 times.", err)
+
+ def test_create__user_allowed_to_authenticate_to_repeated(self):
+ """Test repeating similar arguments doesn't make sense to use together.
+
+ --user-allowed-to-authenticate-to
+ --user-allowed-to-authenticate-to-by-silo
+ """
+ sddl = 'O:SYG:SYD:(XA;OICI;CR;;;WD;(@USER.ad://ext/AuthenticationSilo == "Developers"))'
+ name = self.unique_name()
+
+ result, out, err = self.runcmd("domain", "auth", "policy", "create",
+ "--name", name,
+ "--user-allowed-to-authenticate-to",
+ sddl,
+ "--user-allowed-to-authenticate-to-by-silo",
+ "Managers")
+
+ self.assertEqual(result, -1)
+ self.assertIn("--user-allowed-to-authenticate-to argument repeated 2 times.", err)
+
+ def test_create__service_allowed_to_authenticate_from_repeated(self):
+ """Test repeating similar arguments doesn't make sense to use together.
+
+ --service-allowed-to-authenticate-from
+ --service-allowed-to-authenticate-from-device-silo
+ """
+ sddl = 'O:SYG:SYD:(XA;OICI;CR;;;WD;(@USER.ad://ext/AuthenticationSilo == "Managers"))'
+ name = self.unique_name()
+
+ result, out, err = self.runcmd("domain", "auth", "policy", "create",
+ "--name", name,
+ "--service-allowed-to-authenticate-from",
+ sddl,
+ "--service-allowed-to-authenticate-from-device-silo",
+ "QA")
+
+ self.assertEqual(result, -1)
+ self.assertIn("--service-allowed-to-authenticate-from argument repeated 2 times.", err)
+
+ def test_create__service_allowed_to_authenticate_to_repeated(self):
+ """Test repeating similar arguments doesn't make sense to use together.
+
+ --service-allowed-to-authenticate-to
+ --service-allowed-to-authenticate-to-by-silo
+ """
+ sddl = 'O:SYG:SYD:(XA;OICI;CR;;;WD;(@USER.ad://ext/AuthenticationSilo == "Managers"))'
+ name = self.unique_name()
+
+ result, out, err = self.runcmd("domain", "auth", "policy", "create",
+ "--name", name,
+ "--service-allowed-to-authenticate-to",
+ sddl,
+ "--service-allowed-to-authenticate-to-by-silo",
+ "QA")
+
+ self.assertEqual(result, -1)
+ self.assertIn("--service-allowed-to-authenticate-to argument repeated 2 times.", err)
+
+ def test_create__computer_allowed_to_authenticate_to_repeated(self):
+ """Test repeating similar arguments doesn't make sense to use together.
+
+ --computer-allowed-to-authenticate-to
+ --computer-allowed-to-authenticate-to-by-silo
+ """
+ sddl = 'O:SYG:SYD:(XA;OICI;CR;;;WD;(@USER.ad://ext/AuthenticationSilo == "Managers"))'
+ name = self.unique_name()
+
+ result, out, err = self.runcmd("domain", "auth", "policy", "create",
+ "--name", name,
+ "--computer-allowed-to-authenticate-to",
+ sddl,
+ "--computer-allowed-to-authenticate-to-by-silo",
+ "QA")
+
+ self.assertEqual(result, -1)
+ self.assertIn("--computer-allowed-to-authenticate-to argument repeated 2 times.", err)
+
+ def test_create__fails(self):
+ """Test creating an authentication policy, but it fails."""
+ name = self.unique_name()
+
+ # Raise ModelError when ldb.add() is called.
+ with patch.object(SamDB, "add") as add_mock:
+ add_mock.side_effect = ModelError("Custom error message")
+ result, out, err = self.runcmd("domain", "auth", "policy", "create",
+ "--name", name)
+ self.assertEqual(result, -1)
+ self.assertIn("Custom error message", err)
+
+ def test_modify__description(self):
+ """Test modifying an authentication policy description."""
+ name = self.unique_name()
+
+ # Create a policy to modify for this test.
+ self.addCleanup(self.delete_authentication_policy, name=name, force=True)
+ self.runcmd("domain", "auth", "policy", "create", "--name", name)
+
+ # Change the policy description.
+ result, out, err = self.runcmd("domain", "auth", "policy", "modify",
+ "--name", name,
+ "--description", "NewDescription")
+ self.assertIsNone(result, msg=err)
+
+ # Verify fields were changed.
+ policy = self.get_authentication_policy(name)
+ self.assertEqual(str(policy["description"]), "NewDescription")
+
+ def test_modify__strong_ntlm_policy(self):
+ """Test modify strong ntlm policy on the authentication policy."""
+ name = self.unique_name()
+
+ # Create a policy to modify for this test.
+ self.addCleanup(self.delete_authentication_policy, name=name, force=True)
+ self.runcmd("domain", "auth", "policy", "create", "--name", name)
+
+ result, out, err = self.runcmd("domain", "auth", "policy", "modify",
+ "--name", name,
+ "--strong-ntlm-policy", "Required")
+ self.assertIsNone(result, msg=err)
+
+ # Verify fields were changed.
+ policy = self.get_authentication_policy(name)
+ self.assertEqual(str(policy["msDS-StrongNTLMPolicy"]), "2")
+
+ # Check an invalid choice.
+ with self.assertRaises((OptionValueError, SystemExit)):
+ self.runcmd("domain", "auth", "policy", "modify",
+ "--name", name,
+ "--strong-ntlm-policy", "Invalid")
+
+ # It is difficult to test the error message text for invalid
+ # choices because inside optparse it will raise OptionValueError
+ # followed by raising SystemExit(2).
+
+ def test_modify__user_tgt_lifetime_mins(self):
+ """Test modifying an authentication policy --user-tgt-lifetime-mins.
+
+ This includes checking the upper and lower bounds.
+ """
+ name = self.unique_name()
+
+ # Create a policy to modify for this test.
+ self.addCleanup(self.delete_authentication_policy, name=name, force=True)
+ self.runcmd("domain", "auth", "policy", "create", "--name", name)
+
+ result, out, err = self.runcmd("domain", "auth", "policy", "modify",
+ "--name", name,
+ "--user-tgt-lifetime-mins", "120")
+ self.assertIsNone(result, msg=err)
+
+ # Verify field was changed.
+ policy = self.get_authentication_policy(name)
+ self.assertEqual(str(policy["msDS-UserTGTLifetime"]), "120")
+
+ # check lower bounds (45)
+ result, out, err = self.runcmd("domain", "auth", "policy", "modify",
+ "--name", name + "Lower",
+ "--user-tgt-lifetime-mins", "44")
+ self.assertEqual(result, -1)
+ self.assertIn("--user-tgt-lifetime-mins must be between 45 and 2147483647",
+ err)
+
+ # check upper bounds (2147483647)
+ result, out, err = self.runcmd("domain", "auth", "policy", "modify",
+ "--name", name + "Upper",
+ "--user-tgt-lifetime-mins", "2147483648")
+ self.assertEqual(result, -1)
+ self.assertIn("--user-tgt-lifetime-mins must be between 45 and 2147483647",
+ err)
+
+ def test_modify__service_tgt_lifetime_mins(self):
+ """Test modifying an authentication policy --service-tgt-lifetime-mins.
+
+ This includes checking the upper and lower bounds.
+ """
+ name = self.unique_name()
+
+ # Create a policy to modify for this test.
+ self.addCleanup(self.delete_authentication_policy, name=name, force=True)
+ self.runcmd("domain", "auth", "policy", "create", "--name", name)
+
+ result, out, err = self.runcmd("domain", "auth", "policy", "modify",
+ "--name", name,
+ "--service-tgt-lifetime-mins", "120")
+ self.assertIsNone(result, msg=err)
+
+ # Verify field was changed.
+ policy = self.get_authentication_policy(name)
+ self.assertEqual(str(policy["msDS-ServiceTGTLifetime"]), "120")
+
+ # check lower bounds (45)
+ result, out, err = self.runcmd("domain", "auth", "policy", "modify",
+ "--name", name + "Lower",
+ "--service-tgt-lifetime-mins", "44")
+ self.assertEqual(result, -1)
+ self.assertIn("--service-tgt-lifetime-mins must be between 45 and 2147483647",
+ err)
+
+ # check upper bounds (2147483647)
+ result, out, err = self.runcmd("domain", "auth", "policy", "modify",
+ "--name", name + "Upper",
+ "--service-tgt-lifetime-mins", "2147483648")
+ self.assertEqual(result, -1)
+ self.assertIn("--service-tgt-lifetime-mins must be between 45 and 2147483647",
+ err)
+
+ def test_modify__computer_tgt_lifetime_mins(self):
+ """Test modifying an authentication policy --computer-tgt-lifetime-mins.
+
+ This includes checking the upper and lower bounds.
+ """
+ name = self.unique_name()
+
+ # Create a policy to modify for this test.
+ self.addCleanup(self.delete_authentication_policy, name=name, force=True)
+ self.runcmd("domain", "auth", "policy", "create", "--name", name)
+
+ result, out, err = self.runcmd("domain", "auth", "policy", "modify",
+ "--name", name,
+ "--computer-tgt-lifetime-mins", "120")
+ self.assertIsNone(result, msg=err)
+
+ # Verify field was changed.
+ policy = self.get_authentication_policy(name)
+ self.assertEqual(str(policy["msDS-ComputerTGTLifetime"]), "120")
+
+ # check lower bounds (45)
+ result, out, err = self.runcmd("domain", "auth", "policy", "modify",
+ "--name", name + "Lower",
+ "--computer-tgt-lifetime-mins", "44")
+ self.assertEqual(result, -1)
+ self.assertIn("--computer-tgt-lifetime-mins must be between 45 and 2147483647",
+ err)
+
+ # check upper bounds (2147483647)
+ result, out, err = self.runcmd("domain", "auth", "policy", "modify",
+ "--name", name + "Upper",
+ "--computer-tgt-lifetime-mins", "2147483648")
+ self.assertEqual(result, -1)
+ self.assertIn("--computer-tgt-lifetime-mins must be between 45 and 2147483647",
+ err)
+
+ def test_modify__user_allowed_to_authenticate_from(self):
+ """Modify authentication policy user allowed to authenticate from."""
+ name = self.unique_name()
+ expected = "O:SYG:SYD:(XA;OICI;CR;;;WD;(Member_of {SID(AO)}))"
+
+ # Create a policy to modify for this test.
+ self.addCleanup(self.delete_authentication_policy, name=name, force=True)
+ self.runcmd("domain", "auth", "policy", "create", "--name", name)
+
+ # Modify user allowed to authenticate from field
+ result, out, err = self.runcmd("domain", "auth", "policy", "modify",
+ "--name", name,
+ "--user-allowed-to-authenticate-from",
+ expected)
+ self.assertIsNone(result, msg=err)
+
+ # Check user allowed to authenticate from field was modified.
+ policy = self.get_authentication_policy(name)
+ self.assertEqual(str(policy["cn"]), name)
+ desc = policy["msDS-UserAllowedToAuthenticateFrom"][0]
+ sddl = ndr_unpack(security.descriptor, desc).as_sddl()
+ self.assertEqual(sddl, expected)
+
+ def test_modify__user_allowed_to_authenticate_from_device_group(self):
+ """Test the --user-allowed-to-authenticate-from-device-group shortcut."""
+ name = self.unique_name()
+ expected = "O:SYG:SYD:(XA;OICI;CR;;;WD;(Member_of_any {SID(%s)}))" % (
+ self.device_group.object_sid)
+
+ # Create a policy to modify for this test.
+ self.addCleanup(self.delete_authentication_policy, name=name, force=True)
+ self.runcmd("domain", "auth", "policy", "create", "--name", name)
+
+ # Modify user allowed to authenticate from silo field
+ result, out, err = self.runcmd("domain", "auth", "policy", "modify",
+ "--name", name,
+ "--user-allowed-to-authenticate-from-device-group",
+ self.device_group.name)
+ self.assertIsNone(result, msg=err)
+
+ # Check generated SDDL.
+ policy = self.get_authentication_policy(name)
+ desc = policy["msDS-UserAllowedToAuthenticateFrom"][0]
+ sddl = ndr_unpack(security.descriptor, desc).as_sddl()
+ self.assertEqual(sddl, expected)
+
+ def test_modify__user_allowed_to_authenticate_from_device_silo(self):
+ """Test the --user-allowed-to-authenticate-from-device-silo shortcut."""
+ name = self.unique_name()
+
+ # Create a policy to modify for this test.
+ self.addCleanup(self.delete_authentication_policy, name=name, force=True)
+ self.runcmd("domain", "auth", "policy", "create", "--name", name)
+
+ # Modify user allowed to authenticate from silo field
+ result, out, err = self.runcmd("domain", "auth", "policy", "modify",
+ "--name", name,
+ "--user-allowed-to-authenticate-from-device-silo",
+ "QA")
+ self.assertIsNone(result, msg=err)
+
+ # Check generated SDDL.
+ policy = self.get_authentication_policy(name)
+ desc = policy["msDS-UserAllowedToAuthenticateFrom"][0]
+ sddl = ndr_unpack(security.descriptor, desc).as_sddl()
+ self.assertEqual(
+ sddl,
+ 'O:SYG:SYD:(XA;OICI;CR;;;WD;(@USER.ad://ext/AuthenticationSilo == "QA"))')
+
+ def test_modify__user_allowed_to_authenticate_to(self):
+ """Modify authentication policy user allowed to authenticate to."""
+ name = self.unique_name()
+ expected = "O:SYG:SYD:(XA;OICI;CR;;;WD;(Member_of {SID(AO)}))"
+
+ # Create a policy to modify for this test.
+ self.addCleanup(self.delete_authentication_policy, name=name, force=True)
+ self.runcmd("domain", "auth", "policy", "create", "--name", name)
+
+ # Modify user allowed to authenticate to field
+ result, out, err = self.runcmd("domain", "auth", "policy", "modify",
+ "--name", name,
+ "--user-allowed-to-authenticate-to",
+ expected)
+ self.assertIsNone(result, msg=err)
+
+ # Check user allowed to authenticate to field was modified.
+ policy = self.get_authentication_policy(name)
+ self.assertEqual(str(policy["cn"]), name)
+ desc = policy["msDS-UserAllowedToAuthenticateTo"][0]
+ sddl = ndr_unpack(security.descriptor, desc).as_sddl()
+ self.assertEqual(sddl, expected)
+
+ def test_modify__user_allowed_to_authenticate_to_by_group(self):
+ """Tests the --user-allowed-to-authenticate-to-by-group shortcut."""
+ name = self.unique_name()
+ expected = "O:SYG:SYD:(XA;OICI;CR;;;WD;(Member_of_any {SID(%s)}))" % (
+ self.device_group.object_sid)
+
+ # Create a policy to modify for this test.
+ self.addCleanup(self.delete_authentication_policy, name=name, force=True)
+ self.runcmd("domain", "auth", "policy", "create", "--name", name)
+
+ # Modify user allowed to authenticate to field
+ result, out, err = self.runcmd("domain", "auth", "policy", "modify",
+ "--name", name,
+ "--user-allowed-to-authenticate-to-by-group",
+ self.device_group.name)
+ self.assertIsNone(result, msg=err)
+
+ # Check user allowed to authenticate to field was modified.
+ policy = self.get_authentication_policy(name)
+ self.assertEqual(str(policy["cn"]), name)
+ desc = policy["msDS-UserAllowedToAuthenticateTo"][0]
+ sddl = ndr_unpack(security.descriptor, desc).as_sddl()
+ self.assertEqual(sddl, expected)
+
+ def test_modify__user_allowed_to_authenticate_to_by_silo(self):
+ """Tests the --user-allowed-to-authenticate-to-by-silo shortcut."""
+ name = self.unique_name()
+ expected = ('O:SYG:SYD:(XA;OICI;CR;;;WD;(@USER.ad://ext/'
+ 'AuthenticationSilo == "Developers"))')
+
+ # Create a policy to modify for this test.
+ self.addCleanup(self.delete_authentication_policy, name=name, force=True)
+ self.runcmd("domain", "auth", "policy", "create", "--name", name)
+
+ # Modify user allowed to authenticate to field
+ result, out, err = self.runcmd("domain", "auth", "policy", "modify",
+ "--name", name,
+ "--user-allowed-to-authenticate-to-by-silo",
+ "Developers")
+ self.assertIsNone(result, msg=err)
+
+ # Check user allowed to authenticate to field was modified.
+ policy = self.get_authentication_policy(name)
+ self.assertEqual(str(policy["cn"]), name)
+ desc = policy["msDS-UserAllowedToAuthenticateTo"][0]
+ sddl = ndr_unpack(security.descriptor, desc).as_sddl()
+ self.assertEqual(sddl, expected)
+
+ def test_modify__service_allowed_to_authenticate_from(self):
+ """Modify authentication policy service allowed to authenticate from."""
+ name = self.unique_name()
+ expected = "O:SYG:SYD:(XA;OICI;CR;;;WD;(Member_of {SID(AO)}))"
+
+ # Create a policy to modify for this test.
+ self.addCleanup(self.delete_authentication_policy, name=name, force=True)
+ self.runcmd("domain", "auth", "policy", "create", "--name", name)
+
+ # Modify service allowed to authenticate from field
+ result, out, err = self.runcmd("domain", "auth", "policy", "modify",
+ "--name", name,
+ "--service-allowed-to-authenticate-from",
+ expected)
+ self.assertIsNone(result, msg=err)
+
+ # Check service allowed to authenticate from field was modified.
+ policy = self.get_authentication_policy(name)
+ self.assertEqual(str(policy["cn"]), name)
+ desc = policy["msDS-ServiceAllowedToAuthenticateFrom"][0]
+ sddl = ndr_unpack(security.descriptor, desc).as_sddl()
+ self.assertEqual(sddl, expected)
+
+ def test_modify__service_allowed_to_authenticate_from_device_group(self):
+ """Test the --service-allowed-to-authenticate-from-device-group shortcut."""
+ name = self.unique_name()
+ expected = "O:SYG:SYD:(XA;OICI;CR;;;WD;(Member_of_any {SID(%s)}))" % (
+ self.device_group.object_sid)
+
+ # Create a policy to modify for this test.
+ self.addCleanup(self.delete_authentication_policy, name=name, force=True)
+ self.runcmd("domain", "auth", "policy", "create", "--name", name)
+
+ # Modify user allowed to authenticate from silo field
+ result, out, err = self.runcmd("domain", "auth", "policy", "modify",
+ "--name", name,
+ "--service-allowed-to-authenticate-from-device-group",
+ self.device_group.name)
+ self.assertIsNone(result, msg=err)
+
+ # Check generated SDDL.
+ policy = self.get_authentication_policy(name)
+ desc = policy["msDS-ServiceAllowedToAuthenticateFrom"][0]
+ sddl = ndr_unpack(security.descriptor, desc).as_sddl()
+ self.assertEqual(sddl, expected)
+
+ def test_modify__service_allowed_to_authenticate_from_device_silo(self):
+ """Test the --service-allowed-to-authenticate-from-device-silo shortcut."""
+ name = self.unique_name()
+
+ # Create a policy to modify for this test.
+ self.addCleanup(self.delete_authentication_policy, name=name, force=True)
+ self.runcmd("domain", "auth", "policy", "create", "--name", name)
+
+ # Modify user allowed to authenticate from silo field
+ result, out, err = self.runcmd("domain", "auth", "policy", "modify",
+ "--name", name,
+ "--service-allowed-to-authenticate-from-device-silo",
+ "Developers")
+ self.assertIsNone(result, msg=err)
+
+ # Check generated SDDL.
+ policy = self.get_authentication_policy(name)
+ desc = policy["msDS-ServiceAllowedToAuthenticateFrom"][0]
+ sddl = ndr_unpack(security.descriptor, desc).as_sddl()
+ self.assertEqual(
+ sddl,
+ 'O:SYG:SYD:(XA;OICI;CR;;;WD;(@USER.ad://ext/AuthenticationSilo == "Developers"))')
+
+ def test_modify__service_allowed_to_authenticate_to(self):
+ """Modify authentication policy service allowed to authenticate to."""
+ name = self.unique_name()
+ expected = "O:SYG:SYD:(XA;OICI;CR;;;WD;(Member_of {SID(AO)}))"
+
+ # Create a policy to modify for this test.
+ self.addCleanup(self.delete_authentication_policy, name=name, force=True)
+ self.runcmd("domain", "auth", "policy", "create", "--name", name)
+
+ # Modify service allowed to authenticate to field
+ result, out, err = self.runcmd("domain", "auth", "policy", "modify",
+ "--name", name,
+ "--service-allowed-to-authenticate-to",
+ expected)
+ self.assertIsNone(result, msg=err)
+
+ # Check service allowed to authenticate to field was modified.
+ policy = self.get_authentication_policy(name)
+ self.assertEqual(str(policy["cn"]), name)
+ desc = policy["msDS-ServiceAllowedToAuthenticateTo"][0]
+ sddl = ndr_unpack(security.descriptor, desc).as_sddl()
+ self.assertEqual(sddl, expected)
+
+ def test_modify__service_allowed_to_authenticate_to_by_group(self):
+ """Tests the --service-allowed-to-authenticate-to-by-group shortcut."""
+ name = self.unique_name()
+ expected = "O:SYG:SYD:(XA;OICI;CR;;;WD;(Member_of_any {SID(%s)}))" % (
+ self.device_group.object_sid)
+
+ # Create a policy to modify for this test.
+ self.addCleanup(self.delete_authentication_policy, name=name, force=True)
+ self.runcmd("domain", "auth", "policy", "create", "--name", name)
+
+ # Modify user allowed to authenticate to field
+ result, out, err = self.runcmd("domain", "auth", "policy", "modify",
+ "--name", name,
+ "--service-allowed-to-authenticate-to-by-group",
+ self.device_group.name)
+ self.assertIsNone(result, msg=err)
+
+ # Check user allowed to authenticate to field was modified.
+ policy = self.get_authentication_policy(name)
+ self.assertEqual(str(policy["cn"]), name)
+ desc = policy["msDS-ServiceAllowedToAuthenticateTo"][0]
+ sddl = ndr_unpack(security.descriptor, desc).as_sddl()
+ self.assertEqual(sddl, expected)
+
+ def test_modify__service_allowed_to_authenticate_to_by_silo(self):
+ """Tests the --service-allowed-to-authenticate-to-by-silo shortcut."""
+ name = self.unique_name()
+ expected = ('O:SYG:SYD:(XA;OICI;CR;;;WD;(@USER.ad://ext/'
+ 'AuthenticationSilo == "QA"))')
+
+ # Create a policy to modify for this test.
+ self.addCleanup(self.delete_authentication_policy, name=name, force=True)
+ self.runcmd("domain", "auth", "policy", "create", "--name", name)
+
+ # Modify user allowed to authenticate to field
+ result, out, err = self.runcmd("domain", "auth", "policy", "modify",
+ "--name", name,
+ "--service-allowed-to-authenticate-to-by-silo",
+ "QA")
+ self.assertIsNone(result, msg=err)
+
+ # Check user allowed to authenticate to field was modified.
+ policy = self.get_authentication_policy(name)
+ self.assertEqual(str(policy["cn"]), name)
+ desc = policy["msDS-ServiceAllowedToAuthenticateTo"][0]
+ sddl = ndr_unpack(security.descriptor, desc).as_sddl()
+ self.assertEqual(sddl, expected)
+
+ def test_modify__computer_allowed_to_authenticate_to(self):
+ """Modify authentication policy computer allowed to authenticate to."""
+ name = self.unique_name()
+ expected = "O:SYG:SYD:(XA;OICI;CR;;;WD;(Member_of {SID(AO)}))"
+
+ # Create a policy to modify for this test.
+ self.addCleanup(self.delete_authentication_policy, name=name, force=True)
+ self.runcmd("domain", "auth", "policy", "create", "--name", name)
+
+ # Modify computer allowed to authenticate to field
+ result, out, err = self.runcmd("domain", "auth", "policy", "modify",
+ "--name", name,
+ "--computer-allowed-to-authenticate-to",
+ expected)
+ self.assertIsNone(result, msg=err)
+
+ # Check computer allowed to authenticate to field was modified.
+ policy = self.get_authentication_policy(name)
+ self.assertEqual(str(policy["cn"]), name)
+ desc = policy["msDS-ComputerAllowedToAuthenticateTo"][0]
+ sddl = ndr_unpack(security.descriptor, desc).as_sddl()
+ self.assertEqual(sddl, expected)
+
+ def test_modify__computer_allowed_to_authenticate_to_by_group(self):
+ """Tests the --computer-allowed-to-authenticate-to-by-group shortcut."""
+ name = self.unique_name()
+ expected = "O:SYG:SYD:(XA;OICI;CR;;;WD;(Member_of_any {SID(%s)}))" % (
+ self.device_group.object_sid)
+
+ # Create a policy to modify for this test.
+ self.addCleanup(self.delete_authentication_policy, name=name, force=True)
+ self.runcmd("domain", "auth", "policy", "create", "--name", name)
+
+ # Modify user allowed to authenticate to field
+ result, out, err = self.runcmd("domain", "auth", "policy", "modify",
+ "--name", name,
+ "--computer-allowed-to-authenticate-to-by-group",
+ self.device_group.name)
+ self.assertIsNone(result, msg=err)
+
+ # Check user allowed to authenticate to field was modified.
+ policy = self.get_authentication_policy(name)
+ self.assertEqual(str(policy["cn"]), name)
+ desc = policy["msDS-ComputerAllowedToAuthenticateTo"][0]
+ sddl = ndr_unpack(security.descriptor, desc).as_sddl()
+ self.assertEqual(sddl, expected)
+
+ def test_modify__computer_allowed_to_authenticate_to_by_silo(self):
+ """Tests the --computer-allowed-to-authenticate-to-by-silo shortcut."""
+ name = self.unique_name()
+ expected = ('O:SYG:SYD:(XA;OICI;CR;;;WD;(@USER.ad://ext/'
+ 'AuthenticationSilo == "QA"))')
+
+ # Create a policy to modify for this test.
+ self.addCleanup(self.delete_authentication_policy, name=name, force=True)
+ self.runcmd("domain", "auth", "policy", "create", "--name", name)
+
+ # Modify user allowed to authenticate to field
+ result, out, err = self.runcmd("domain", "auth", "policy", "modify",
+ "--name", name,
+ "--computer-allowed-to-authenticate-to-by-silo",
+ "QA")
+ self.assertIsNone(result, msg=err)
+
+ # Check user allowed to authenticate to field was modified.
+ policy = self.get_authentication_policy(name)
+ self.assertEqual(str(policy["cn"]), name)
+ desc = policy["msDS-ComputerAllowedToAuthenticateTo"][0]
+ sddl = ndr_unpack(security.descriptor, desc).as_sddl()
+ self.assertEqual(sddl, expected)
+
+ def test_modify__name_missing(self):
+ """Test modify authentication but the --name argument is missing."""
+ result, out, err = self.runcmd("domain", "auth", "policy", "modify",
+ "--description", "NewDescription")
+ self.assertEqual(result, -1)
+ self.assertIn("Argument --name is required.", err)
+
+ def test_modify__notfound(self):
+ """Test modify an authentication silo that doesn't exist."""
+ result, out, err = self.runcmd("domain", "auth", "policy", "modify",
+ "--name", "doesNotExist",
+ "--description", "NewDescription")
+ self.assertEqual(result, -1)
+ self.assertIn("Authentication policy doesNotExist not found.", err)
+
+ def test_modify__audit_enforce(self):
+ """Test modify authentication policy using --audit and --enforce."""
+ name = self.unique_name()
+
+ # Create a policy to modify for this test.
+ self.addCleanup(self.delete_authentication_policy,
+ name=name, force=True)
+ self.runcmd("domain", "auth", "policy", "create", "--name", name)
+
+ # Change to audit, the default is --enforce.
+ result, out, err = self.runcmd("domain", "auth", "policy", "modify",
+ "--name", name,
+ "--audit")
+ self.assertIsNone(result, msg=err)
+
+ # Check that the policy was changed to --audit.
+ policy = self.get_authentication_policy(name)
+ self.assertEqual(str(policy["msDS-AuthNPolicyEnforced"]), "FALSE")
+
+ result, out, err = self.runcmd("domain", "auth", "policy", "modify",
+ "--name", name,
+ "--enforce")
+ self.assertIsNone(result, msg=err)
+
+ # Check if the policy was changed back to --enforce.
+ policy = self.get_authentication_policy(name)
+ self.assertEqual(str(policy["msDS-AuthNPolicyEnforced"]), "TRUE")
+
+ def test_modify__protect_unprotect(self):
+ """Test modify authentication policy using --protect and --unprotect."""
+ name = self.unique_name()
+
+ # Create a policy to modify for this test.
+ self.addCleanup(self.delete_authentication_policy, name=name, force=True)
+ self.runcmd("domain", "auth", "policy", "create", "--name", name)
+
+ utils = SDUtils(self.samdb)
+ result, out, err = self.runcmd("domain", "auth", "policy", "modify",
+ "--name", name,
+ "--protect")
+ self.assertIsNone(result, msg=err)
+
+ # Check that claim type was protected.
+ policy = self.get_authentication_policy(name)
+ desc = utils.get_sd_as_sddl(policy["dn"])
+ self.assertIn("(D;;DTSD;;;WD)", desc)
+
+ result, out, err = self.runcmd("domain", "auth", "policy", "modify",
+ "--name", name,
+ "--unprotect")
+ self.assertIsNone(result, msg=err)
+
+ # Check that claim type was unprotected.
+ policy = self.get_authentication_policy(name)
+ desc = utils.get_sd_as_sddl(policy["dn"])
+ self.assertNotIn("(D;;DTSD;;;WD)", desc)
+
+ def test_modify__audit_enforce_together(self):
+ """Test modify auth policy using both --audit and --enforce."""
+ result, out, err = self.runcmd("domain", "auth", "policy", "modify",
+ "--name", "User Policy",
+ "--audit", "--enforce")
+ self.assertEqual(result, -1)
+ self.assertIn("--audit and --enforce cannot be used together.", err)
+
+ def test_modify__protect_unprotect_together(self):
+ """Test modify authentication policy using --protect and --unprotect."""
+ result, out, err = self.runcmd("domain", "auth", "policy", "modify",
+ "--name", "User Policy",
+ "--protect", "--unprotect")
+ self.assertEqual(result, -1)
+ self.assertIn("--protect and --unprotect cannot be used together.", err)
+
+ def test_modify__fails(self):
+ """Test modifying an authentication policy, but it fails."""
+ # Raise ModelError when ldb.add() is called.
+ with patch.object(SamDB, "modify") as modify_mock:
+ modify_mock.side_effect = ModelError("Custom error message")
+ result, out, err = self.runcmd("domain", "auth", "policy", "modify",
+ "--name", "User Policy",
+ "--description", "New description")
+ self.assertEqual(result, -1)
+ self.assertIn("Custom error message", err)
+
+ def test_delete__success(self):
+ """Test deleting an authentication policy that is not protected."""
+ # Create non-protected authentication policy.
+ result, out, err = self.runcmd("domain", "auth", "policy", "create",
+ "--name=deleteTest")
+ self.assertIsNone(result, msg=err)
+ policy = self.get_authentication_policy("deleteTest")
+ self.assertIsNotNone(policy)
+
+ # Do the deletion.
+ result, out, err = self.runcmd("domain", "auth", "policy", "delete",
+ "--name", "deleteTest")
+ self.assertIsNone(result, msg=err)
+
+ # Authentication policy shouldn't exist anymore.
+ policy = self.get_authentication_policy("deleteTest")
+ self.assertIsNone(policy)
+
+ def test_delete__protected(self):
+ """Test deleting a protected auth policy, with and without --force."""
+ # Create protected authentication policy.
+ result, out, err = self.runcmd("domain", "auth", "policy", "create",
+ "--name=deleteProtected",
+ "--protect")
+ self.assertIsNone(result, msg=err)
+ policy = self.get_authentication_policy("deleteProtected")
+ self.assertIsNotNone(policy)
+
+ # Do the deletion.
+ result, out, err = self.runcmd("domain", "auth", "policy", "delete",
+ "--name=deleteProtected")
+ self.assertEqual(result, -1)
+
+ # Authentication silo should still exist.
+ policy = self.get_authentication_policy("deleteProtected")
+ self.assertIsNotNone(policy)
+
+ # Try a force delete instead.
+ result, out, err = self.runcmd("domain", "auth", "policy", "delete",
+ "--name=deleteProtected", "--force")
+ self.assertIsNone(result, msg=err)
+
+ # Authentication silo shouldn't exist anymore.
+ policy = self.get_authentication_policy("deleteProtected")
+ self.assertIsNone(policy)
+
+ def test_delete__notfound(self):
+ """Test deleting an authentication policy that doesn't exist."""
+ result, out, err = self.runcmd("domain", "auth", "policy", "delete",
+ "--name", "doesNotExist")
+ self.assertEqual(result, -1)
+ self.assertIn("Authentication policy doesNotExist not found.", err)
+
+ def test_delete__name_required(self):
+ """Test deleting an authentication policy without --name argument."""
+ result, out, err = self.runcmd("domain", "auth", "policy", "delete")
+ self.assertEqual(result, -1)
+ self.assertIn("Argument --name is required.", err)
+
+ def test_delete__force_fails(self):
+ """Test deleting an authentication policy with --force, but it fails."""
+ name = self.unique_name()
+
+ # Create protected authentication policy.
+ self.addCleanup(self.delete_authentication_policy, name=name, force=True)
+ result, out, err = self.runcmd("domain", "auth", "policy", "create",
+ "--name", name,
+ "--protect")
+ self.assertIsNone(result, msg=err)
+
+ # Policy exists
+ policy = self.get_authentication_policy(name)
+ self.assertIsNotNone(policy)
+
+ # Try doing delete with --force.
+ # Patch SDUtils.dacl_delete_aces with a Mock that raises ModelError.
+ with patch.object(SDUtils, "dacl_delete_aces") as delete_mock:
+ delete_mock.side_effect = ModelError("Custom error message")
+ result, out, err = self.runcmd("domain", "auth", "policy", "delete",
+ "--name", name,
+ "--force")
+ self.assertEqual(result, -1)
+ self.assertIn("Custom error message", err)
+
+ def test_delete__fails(self):
+ """Test deleting an authentication policy, but it fails."""
+ name = self.unique_name()
+
+ # Create regular authentication policy.
+ self.addCleanup(self.delete_authentication_policy, name=name, force=True)
+ result, out, err = self.runcmd("domain", "auth", "policy", "create",
+ "--name", name)
+ self.assertIsNone(result, msg=err)
+
+ # Policy exists
+ policy = self.get_authentication_policy(name)
+ self.assertIsNotNone(policy)
+
+ # Raise ModelError when ldb.delete() is called.
+ with patch.object(SamDB, "delete") as delete_mock:
+ delete_mock.side_effect = ModelError("Custom error message")
+ result, out, err = self.runcmd("domain", "auth", "policy", "delete",
+ "--name", name)
+ self.assertEqual(result, -1)
+ self.assertIn("Custom error message", err)
+
+ # When not using --force we get a hint.
+ self.assertIn("Try --force", err)
+
+ def test_delete__protected_fails(self):
+ """Test deleting an authentication policy, but it fails."""
+ name = self.unique_name()
+
+ # Create protected authentication policy.
+ self.addCleanup(self.delete_authentication_policy, name=name, force=True)
+ result, out, err = self.runcmd("domain", "auth", "policy", "create",
+ "--name", name,
+ "--protect")
+ self.assertIsNone(result, msg=err)
+
+ # Policy exists
+ policy = self.get_authentication_policy(name)
+ self.assertIsNotNone(policy)
+
+ # Raise ModelError when ldb.delete() is called.
+ with patch.object(SamDB, "delete") as delete_mock:
+ delete_mock.side_effect = ModelError("Custom error message")
+ result, out, err = self.runcmd("domain", "auth", "policy", "delete",
+ "--name", name,
+ "--force")
+ self.assertEqual(result, -1)
+ self.assertIn("Custom error message", err)
+
+ # When using --force we don't get the hint.
+ self.assertNotIn("Try --force", err)
diff --git a/python/samba/tests/samba_tool/domain_auth_silo.py b/python/samba/tests/samba_tool/domain_auth_silo.py
new file mode 100644
index 0000000..a1cd85c
--- /dev/null
+++ b/python/samba/tests/samba_tool/domain_auth_silo.py
@@ -0,0 +1,618 @@
+# Unix SMB/CIFS implementation.
+#
+# Tests for samba-tool domain auth silo command
+#
+# Copyright (C) Catalyst.Net Ltd. 2023
+#
+# Written by Rob van der Linde <rob@catalyst.net.nz>
+#
+# 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 json
+from unittest.mock import patch
+
+from samba.netcmd.domain.models.exceptions import ModelError
+from samba.samdb import SamDB
+from samba.sd_utils import SDUtils
+
+from .silo_base import SiloTest
+
+
+class AuthSiloCmdTestCase(SiloTest):
+
+ def test_list(self):
+ """Test listing authentication silos in list format."""
+ result, out, err = self.runcmd("domain", "auth", "silo", "list")
+ self.assertIsNone(result, msg=err)
+
+ expected_silos = ["Developers", "Managers", "QA"]
+
+ for silo in expected_silos:
+ self.assertIn(silo, out)
+
+ def test_list___json(self):
+ """Test listing authentication silos in JSON format."""
+ result, out, err = self.runcmd("domain", "auth", "silo",
+ "list", "--json")
+ self.assertIsNone(result, msg=err)
+
+ # we should get valid json
+ silos = json.loads(out)
+
+ expected_silos = ["Developers", "Managers", "QA"]
+
+ for name in expected_silos:
+ silo = silos[name]
+ self.assertIn("msDS-AuthNPolicySilo", list(silo["objectClass"]))
+ self.assertIn("description", silo)
+ self.assertIn("msDS-UserAuthNPolicy", silo)
+ self.assertIn("objectGUID", silo)
+
+ def test_view(self):
+ """Test viewing a single authentication silo."""
+ result, out, err = self.runcmd("domain", "auth", "silo", "view",
+ "--name", "Developers")
+ self.assertIsNone(result, msg=err)
+
+ # we should get valid json
+ silo = json.loads(out)
+
+ # check a few fields only
+ self.assertEqual(silo["cn"], "Developers")
+ self.assertEqual(silo["description"],
+ "Developers, Developers, Developers!")
+
+ def test_view__notfound(self):
+ """Test viewing an authentication silo that doesn't exist."""
+ result, out, err = self.runcmd("domain", "auth", "silo", "view",
+ "--name", "doesNotExist")
+ self.assertEqual(result, -1)
+ self.assertIn("Authentication silo doesNotExist not found.", err)
+
+ def test_view__name_required(self):
+ """Test view authentication silo without --name argument."""
+ result, out, err = self.runcmd("domain", "auth", "silo", "view")
+ self.assertEqual(result, -1)
+ self.assertIn("Argument --name is required.", err)
+
+ def test_create__single_policy(self):
+ """Test creating a new authentication silo with a single policy."""
+ name = self.unique_name()
+
+ self.addCleanup(self.delete_authentication_silo, name=name, force=True)
+ result, out, err = self.runcmd("domain", "auth", "silo", "create",
+ "--name", name,
+ "--user-authentication-policy", "User Policy")
+ self.assertIsNone(result, msg=err)
+
+ # Check silo that was created
+ silo = self.get_authentication_silo(name)
+ self.assertEqual(str(silo["cn"]), name)
+ self.assertIn("User Policy", str(silo["msDS-UserAuthNPolicy"]))
+ self.assertEqual(str(silo["msDS-AuthNPolicySiloEnforced"]), "TRUE")
+
+ def test_create__multiple_policies(self):
+ """Test creating a new authentication silo with multiple policies."""
+ name = self.unique_name()
+
+ self.addCleanup(self.delete_authentication_silo, name=name, force=True)
+ result, out, err = self.runcmd("domain", "auth", "silo", "create",
+ "--name", name,
+ "--user-authentication-policy",
+ "User Policy",
+ "--service-authentication-policy",
+ "Service Policy",
+ "--computer-authentication-policy",
+ "Computer Policy")
+ self.assertIsNone(result, msg=err)
+
+ # Check silo that was created.
+ silo = self.get_authentication_silo(name)
+ self.assertEqual(str(silo["cn"]), name)
+ self.assertIn("User Policy", str(silo["msDS-UserAuthNPolicy"]))
+ self.assertIn("Service Policy", str(silo["msDS-ServiceAuthNPolicy"]))
+ self.assertIn("Computer Policy", str(silo["msDS-ComputerAuthNPolicy"]))
+ self.assertEqual(str(silo["msDS-AuthNPolicySiloEnforced"]), "TRUE")
+
+ def test_create__policy_dn(self):
+ """Test creating a new authentication silo when policy is a dn."""
+ name = self.unique_name()
+ policy = self.get_authentication_policy("User Policy")
+
+ self.addCleanup(self.delete_authentication_silo, name=name, force=True)
+ result, out, err = self.runcmd("domain", "auth", "silo", "create",
+ "--name", name,
+ "--user-authentication-policy", policy["dn"])
+ self.assertIsNone(result, msg=err)
+
+ # Check silo that was created
+ silo = self.get_authentication_silo(name)
+ self.assertEqual(str(silo["cn"]), name)
+ self.assertIn(str(policy["name"]), str(silo["msDS-UserAuthNPolicy"]))
+ self.assertEqual(str(silo["msDS-AuthNPolicySiloEnforced"]), "TRUE")
+
+ def test_create__already_exists(self):
+ """Test creating a new authentication silo that already exists."""
+ result, out, err = self.runcmd("domain", "auth", "silo", "create",
+ "--name", "Developers",
+ "--user-authentication-policy", "User Policy")
+ self.assertEqual(result, -1)
+ self.assertIn("Authentication silo Developers already exists.", err)
+
+ def test_create__name_missing(self):
+ """Test create authentication silo without --name argument."""
+ result, out, err = self.runcmd("domain", "auth", "silo", "create",
+ "--user-authentication-policy", "User Policy")
+ self.assertEqual(result, -1)
+ self.assertIn("Argument --name is required.", err)
+
+ def test_create__audit(self):
+ """Test create authentication silo with --audit flag."""
+ name = self.unique_name()
+
+ self.addCleanup(self.delete_authentication_silo, name=name, force=True)
+ result, out, err = self.runcmd("domain", "auth", "silo", "create",
+ "--name", "auditPolicies",
+ "--user-authentication-policy", "User Policy",
+ "--name", name,
+ "--user-authentication-policy", "User Policy",
+ "--audit")
+ self.assertIsNone(result, msg=err)
+
+ # fetch and check silo
+ silo = self.get_authentication_silo(name)
+ self.assertEqual(str(silo["msDS-AuthNPolicySiloEnforced"]), "FALSE")
+
+ def test_create__enforce(self):
+ """Test create authentication silo with --enforce flag."""
+ name = self.unique_name()
+
+ self.addCleanup(self.delete_authentication_silo, name=name, force=True)
+ result, out, err = self.runcmd("domain", "auth", "silo", "create",
+ "--name", name,
+ "--user-authentication-policy", "User Policy",
+ "--enforce")
+ self.assertIsNone(result, msg=err)
+
+ # fetch and check silo
+ silo = self.get_authentication_silo(name)
+ self.assertEqual(str(silo["msDS-AuthNPolicySiloEnforced"]), "TRUE")
+
+ def test_create__audit_enforce_together(self):
+ """Test create authentication silo using both --audit and --enforce."""
+ name = self.unique_name()
+
+ result, out, err = self.runcmd("domain", "auth", "silo", "create",
+ "--name", name,
+ "--user-authentication-policy", "User Policy",
+ "--audit", "--enforce")
+
+ self.assertEqual(result, -1)
+ self.assertIn("--audit and --enforce cannot be used together.", err)
+
+ def test_create__protect_unprotect_together(self):
+ """Test create authentication silo using --protect and --unprotect."""
+ name = self.unique_name()
+
+ result, out, err = self.runcmd("domain", "auth", "silo", "create",
+ "--name", name,
+ "--user-authentication-policy", "User Policy",
+ "--protect", "--unprotect")
+
+ self.assertEqual(result, -1)
+ self.assertIn("--protect and --unprotect cannot be used together.", err)
+
+ def test_create__policy_notfound(self):
+ """Test create authentication silo with a policy that doesn't exist."""
+ name = self.unique_name()
+
+ result, out, err = self.runcmd("domain", "auth", "silo", "create",
+ "--name", name,
+ "--user-authentication-policy", "Invalid Policy")
+
+ self.assertEqual(result, -1)
+ self.assertIn("Authentication policy Invalid Policy not found.", err)
+
+ def test_create__fails(self):
+ """Test creating an authentication silo, but it fails."""
+ name = self.unique_name()
+
+ # Raise ModelError when ldb.add() is called.
+ with patch.object(SamDB, "add") as add_mock:
+ add_mock.side_effect = ModelError("Custom error message")
+ result, out, err = self.runcmd("domain", "auth", "silo", "create",
+ "--name", name,
+ "--user-authentication-policy", "User Policy")
+ self.assertEqual(result, -1)
+ self.assertIn("Custom error message", err)
+
+ def test_modify__description(self):
+ """Test modify authentication silo changing the description field."""
+ name = self.unique_name()
+
+ # Create a silo to modify for this test.
+ self.addCleanup(self.delete_authentication_silo, name=name, force=True)
+ self.runcmd("domain", "auth", "silo", "create", "--name", name)
+
+ result, out, err = self.runcmd("domain", "auth", "silo", "modify",
+ "--name", name,
+ "--description", "New Description")
+ self.assertIsNone(result, msg=err)
+
+ # check new value
+ silo = self.get_authentication_silo(name)
+ self.assertEqual(str(silo["description"]), "New Description")
+
+ def test_modify__audit_enforce(self):
+ """Test modify authentication silo setting --audit and --enforce."""
+ name = self.unique_name()
+
+ # Create a silo to modify for this test.
+ self.addCleanup(self.delete_authentication_silo, name=name, force=True)
+ self.runcmd("domain", "auth", "silo", "create", "--name", name)
+
+ result, out, err = self.runcmd("domain", "auth", "silo", "modify",
+ "--name", name,
+ "--audit")
+ self.assertIsNone(result, msg=err)
+
+ # Check silo is set to audit.
+ silo = self.get_authentication_silo(name)
+ self.assertEqual(str(silo["msDS-AuthNPolicySiloEnforced"]), "FALSE")
+
+ result, out, err = self.runcmd("domain", "auth", "silo", "modify",
+ "--name", name,
+ "--enforce")
+ self.assertIsNone(result, msg=err)
+
+ # Check is set to enforce.
+ silo = self.get_authentication_silo(name)
+ self.assertEqual(str(silo["msDS-AuthNPolicySiloEnforced"]), "TRUE")
+
+ def test_modify__protect_unprotect(self):
+ """Test modify un-protecting and protecting an authentication silo."""
+ name = self.unique_name()
+
+ # Create a silo to modify for this test.
+ self.addCleanup(self.delete_authentication_silo, name=name, force=True)
+ self.runcmd("domain", "auth", "silo", "create", "--name", name)
+
+ utils = SDUtils(self.samdb)
+ result, out, err = self.runcmd("domain", "auth", "silo", "modify",
+ "--name", name,
+ "--protect")
+ self.assertIsNone(result, msg=err)
+
+ # Check that silo was protected.
+ silo = self.get_authentication_silo(name)
+ desc = utils.get_sd_as_sddl(silo["dn"])
+ self.assertIn("(D;;DTSD;;;WD)", desc)
+
+ result, out, err = self.runcmd("domain", "auth", "silo", "modify",
+ "--name", name,
+ "--unprotect")
+ self.assertIsNone(result, msg=err)
+
+ # Check that silo was unprotected.
+ silo = self.get_authentication_silo(name)
+ desc = utils.get_sd_as_sddl(silo["dn"])
+ self.assertNotIn("(D;;DTSD;;;WD)", desc)
+
+ def test_modify__audit_enforce_together(self):
+ """Test modify silo doesn't allow both --audit and --enforce."""
+ result, out, err = self.runcmd("domain", "auth", "silo", "modify",
+ "--name", "QA",
+ "--audit", "--enforce")
+
+ self.assertEqual(result, -1)
+ self.assertIn("--audit and --enforce cannot be used together.", err)
+
+ def test_modify__protect_unprotect_together(self):
+ """Test modify silo using both --protect and --unprotect."""
+ result, out, err = self.runcmd("domain", "auth", "silo", "modify",
+ "--name", "Developers",
+ "--protect", "--unprotect")
+ self.assertEqual(result, -1)
+ self.assertIn("--protect and --unprotect cannot be used together.", err)
+
+ def test_modify__notfound(self):
+ """Test modify an authentication silo that doesn't exist."""
+ result, out, err = self.runcmd("domain", "auth", "silo", "modify",
+ "--name", "doesNotExist",
+ "--description=NewDescription")
+ self.assertEqual(result, -1)
+ self.assertIn("Authentication silo doesNotExist not found.", err)
+
+ def test_modify__name_missing(self):
+ """Test modify authentication silo without --name argument."""
+ result, out, err = self.runcmd("domain", "auth", "silo", "modify")
+ self.assertEqual(result, -1)
+ self.assertIn("Argument --name is required.", err)
+
+ def test_modify__fails(self):
+ """Test modify authentication silo, but it fails."""
+ # Raise ModelError when ldb.modify() is called.
+ with patch.object(SamDB, "modify") as add_mock:
+ add_mock.side_effect = ModelError("Custom error message")
+ result, out, err = self.runcmd("domain", "auth", "silo", "modify",
+ "--name", "Developers",
+ "--description", "Devs")
+ self.assertEqual(result, -1)
+ self.assertIn("Custom error message", err)
+
+ def test_authentication_silo_delete(self):
+ """Test deleting an authentication silo that is not protected."""
+ name = self.unique_name()
+
+ # Create non-protected authentication silo.
+ result, out, err = self.runcmd("domain", "auth", "silo", "create",
+ "--name", name,
+ "--user-authentication-policy", "User Policy")
+ self.assertIsNone(result, msg=err)
+ silo = self.get_authentication_silo(name)
+ self.assertIsNotNone(silo)
+
+ # Do the deletion.
+ result, out, err = self.runcmd("domain", "auth", "silo", "delete",
+ "--name", name)
+ self.assertIsNone(result, msg=err)
+
+ # Authentication silo shouldn't exist anymore.
+ silo = self.get_authentication_silo(name)
+ self.assertIsNone(silo)
+
+ def test_delete__protected(self):
+ """Test deleting a protected auth silo, with and without --force."""
+ name = self.unique_name()
+
+ # Create protected authentication silo.
+ result, out, err = self.runcmd("domain", "auth", "silo", "create",
+ "--name", name,
+ "--user-authentication-policy", "User Policy",
+ "--protect")
+ self.assertIsNone(result, msg=err)
+ silo = self.get_authentication_silo(name)
+ self.assertIsNotNone(silo)
+
+ # Do the deletion.
+ result, out, err = self.runcmd("domain", "auth", "silo", "delete",
+ "--name", name)
+ self.assertEqual(result, -1)
+
+ # Authentication silo should still exist.
+ silo = self.get_authentication_silo(name)
+ self.assertIsNotNone(silo)
+
+ # Try a force delete instead.
+ result, out, err = self.runcmd("domain", "auth", "silo", "delete",
+ "--name", name, "--force")
+ self.assertIsNone(result, msg=err)
+
+ # Authentication silo shouldn't exist anymore.
+ silo = self.get_authentication_silo(name)
+ self.assertIsNone(silo)
+
+ def test_delete__notfound(self):
+ """Test deleting an authentication silo that doesn't exist."""
+ result, out, err = self.runcmd("domain", "auth", "silo", "delete",
+ "--name", "doesNotExist")
+ self.assertEqual(result, -1)
+ self.assertIn("Authentication silo doesNotExist not found.", err)
+
+ def test_delete__name_required(self):
+ """Test deleting an authentication silo without --name argument."""
+ result, out, err = self.runcmd("domain", "auth", "silo", "delete")
+ self.assertEqual(result, -1)
+ self.assertIn("Argument --name is required.", err)
+
+ def test_delete__force_fails(self):
+ """Test deleting an authentication silo with --force, but it fails."""
+ name = self.unique_name()
+
+ # Create protected authentication silo.
+ self.addCleanup(self.delete_authentication_silo, name=name, force=True)
+ result, out, err = self.runcmd("domain", "auth", "silo", "create",
+ "--name", name,
+ "--user-authentication-policy", "User Policy",
+ "--protect")
+ self.assertIsNone(result, msg=err)
+
+ # Silo exists
+ silo = self.get_authentication_silo(name)
+ self.assertIsNotNone(silo)
+
+ # Try doing delete with --force.
+ # Patch SDUtils.dacl_delete_aces with a Mock that raises ModelError.
+ with patch.object(SDUtils, "dacl_delete_aces") as delete_mock:
+ delete_mock.side_effect = ModelError("Custom error message")
+ result, out, err = self.runcmd("domain", "auth", "silo", "delete",
+ "--name", name,
+ "--force")
+ self.assertEqual(result, -1)
+ self.assertIn("Custom error message", err)
+
+ def test_delete__fails(self):
+ """Test deleting an authentication silo, but it fails."""
+ name = self.unique_name()
+
+ # Create regular authentication silo.
+ self.addCleanup(self.delete_authentication_silo, name=name, force=True)
+ result, out, err = self.runcmd("domain", "auth", "silo", "create",
+ "--name", name,
+ "--user-authentication-policy", "User Policy")
+ self.assertIsNone(result, msg=err)
+
+ # Silo exists
+ silo = self.get_authentication_silo(name)
+ self.assertIsNotNone(silo)
+
+ # Raise ModelError when ldb.delete() is called.
+ with patch.object(SamDB, "delete") as delete_mock:
+ delete_mock.side_effect = ModelError("Custom error message")
+ result, out, err = self.runcmd("domain", "auth", "silo", "delete",
+ "--name", name)
+ self.assertEqual(result, -1)
+ self.assertIn("Custom error message", err)
+
+ # When not using --force we get a hint.
+ self.assertIn("Try --force", err)
+
+ def test_delete__protected_fails(self):
+ """Test deleting an authentication silo, but it fails."""
+ name = self.unique_name()
+
+ # Create protected authentication silo.
+ self.addCleanup(self.delete_authentication_silo, name=name, force=True)
+ result, out, err = self.runcmd("domain", "auth", "silo", "create",
+ "--name", name,
+ "--user-authentication-policy", "User Policy",
+ "--protect")
+ self.assertIsNone(result, msg=err)
+
+ # Silo exists
+ silo = self.get_authentication_silo(name)
+ self.assertIsNotNone(silo)
+
+ # Raise ModelError when ldb.delete() is called.
+ with patch.object(SamDB, "delete") as delete_mock:
+ delete_mock.side_effect = ModelError("Custom error message")
+ result, out, err = self.runcmd("domain", "auth", "silo", "delete",
+ "--name", name,
+ "--force")
+ self.assertEqual(result, -1)
+ self.assertIn("Custom error message", err)
+
+ # When using --force we don't get the hint.
+ self.assertNotIn("Try --force", err)
+
+
+class AuthSiloMemberCmdTestCase(SiloTest):
+
+ def setUp(self):
+ super().setUp()
+
+ # Create an organisational unit to test in.
+ self.ou = self.samdb.get_default_basedn()
+ self.ou.add_child("OU=Domain Auth Tests")
+ self.samdb.create_ou(self.ou)
+ self.addCleanup(self.samdb.delete, self.ou, ["tree_delete:1"])
+
+ # Grant member access to silos
+ self.grant_silo_access("Developers", "bob")
+ self.grant_silo_access("Developers", "jane")
+ self.grant_silo_access("Managers", "alice")
+
+ def create_computer(self, name):
+ """Create a Computer and return the dn."""
+ dn = f"CN={name},{self.ou}"
+ self.samdb.newcomputer(name, self.ou)
+ return dn
+
+ def grant_silo_access(self, silo, member):
+ """Grant a member access to an authentication silo."""
+ result, out, err = self.runcmd("domain", "auth", "silo",
+ "member", "grant",
+ "--name", silo, "--member", member)
+
+ self.assertIsNone(result, msg=err)
+ self.assertIn(
+ f"User {member} granted access to the authentication silo {silo}",
+ out)
+ self.addCleanup(self.revoke_silo_access, silo, member)
+
+ def revoke_silo_access(self, silo, member):
+ """Revoke a member from an authentication silo."""
+ result, out, err = self.runcmd("domain", "auth", "silo",
+ "member", "revoke",
+ "--name", silo, "--member", member)
+
+ self.assertIsNone(result, msg=err)
+
+ def test_member_list(self):
+ """Test listing authentication policy members in list format."""
+ alice = self.get_user("alice")
+ jane = self.get_user("jane")
+ bob = self.get_user("bob")
+
+ result, out, err = self.runcmd("domain", "auth", "silo",
+ "member", "list",
+ "--name", "Developers")
+
+ self.assertIsNone(result, msg=err)
+ self.assertIn(str(bob.dn), out)
+ self.assertIn(str(jane.dn), out)
+ self.assertNotIn(str(alice.dn), out)
+
+ def test_member_list___json(self):
+ """Test listing authentication policy members list in json format."""
+ alice = self.get_user("alice")
+ jane = self.get_user("jane")
+ bob = self.get_user("bob")
+
+ result, out, err = self.runcmd("domain", "auth", "silo",
+ "member", "list",
+ "--name", "Developers", "--json")
+
+ self.assertIsNone(result, msg=err)
+ members = json.loads(out)
+ members_dn = [member["dn"] for member in members]
+ self.assertIn(str(bob.dn), members_dn)
+ self.assertIn(str(jane.dn), members_dn)
+ self.assertNotIn(str(alice.dn), members_dn)
+
+ def test_member_list__name_missing(self):
+ """Test list authentication policy members without the name argument."""
+ result, out, err = self.runcmd("domain", "auth", "silo",
+ "member", "list")
+
+ self.assertIsNotNone(result)
+ self.assertIn("Argument --name is required.", err)
+
+ def test_member_grant__user(self):
+ """Test adding a user to an authentication silo."""
+ self.grant_silo_access("Developers", "joe")
+
+ # Check if member is in silo
+ user = self.get_user("joe")
+ silo = self.get_authentication_silo("Developers")
+ members = [str(member) for member in silo["msDS-AuthNPolicySiloMembers"]]
+ self.assertIn(str(user.dn), members)
+
+ def test_member_grant__computer(self):
+ """Test adding a computer to an authentication silo"""
+ name = self.unique_name()
+ computer = self.create_computer(name)
+ silo = "Developers"
+
+ # Don't use self.grant_silo_member as it will try to clean up the user.
+ result, out, err = self.runcmd("domain", "auth", "silo",
+ "member", "grant",
+ "--name", silo,
+ "--member", computer)
+
+ self.assertIsNone(result, msg=err)
+ self.assertIn(
+ f"User {name}$ granted access to the authentication silo {silo} (unassigned).",
+ out)
+
+ def test_member_grant__unknown_user(self):
+ """Test adding an unknown user to an authentication silo."""
+ result, out, err = self.runcmd("domain", "auth", "silo",
+ "member", "grant",
+ "--name", "Developers",
+ "--member", "does_not_exist")
+
+ self.assertIsNotNone(result)
+ self.assertIn("User does_not_exist not found.", err)
diff --git a/python/samba/tests/samba_tool/domain_claim.py b/python/samba/tests/samba_tool/domain_claim.py
new file mode 100644
index 0000000..96caacd
--- /dev/null
+++ b/python/samba/tests/samba_tool/domain_claim.py
@@ -0,0 +1,608 @@
+# Unix SMB/CIFS implementation.
+#
+# Tests for samba-tool domain claim management
+#
+# Copyright (C) Catalyst.Net Ltd. 2023
+#
+# Written by Rob van der Linde <rob@catalyst.net.nz>
+#
+# 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 json
+import os
+
+from ldb import SCOPE_ONELEVEL
+from samba.sd_utils import SDUtils
+
+from .base import SambaToolCmdTest
+
+# List of claim value types we should expect to see.
+VALUE_TYPES = [
+ "Date Time",
+ "Multi-valued Choice",
+ "Multi-valued Text",
+ "Number",
+ "Ordered List",
+ "Single-valued Choice",
+ "Text",
+ "Yes/No"
+]
+
+HOST = "ldap://{DC_SERVER}".format(**os.environ)
+CREDS = "-U{DC_USERNAME}%{DC_PASSWORD}".format(**os.environ)
+
+
+class BaseClaimCmdTest(SambaToolCmdTest):
+ """Base class for claim types and claim value types tests."""
+
+ @classmethod
+ def setUpClass(cls):
+ cls.samdb = cls.getSamDB("-H", HOST, CREDS)
+ super().setUpClass()
+
+ @classmethod
+ def setUpTestData(cls):
+ cls.create_claim_type("accountExpires", name="expires",
+ classes=["user"])
+ cls.create_claim_type("department", name="dept", classes=["user"],
+ protect=True)
+ cls.create_claim_type("carLicense", name="plate", classes=["user"],
+ disable=True)
+
+ def get_services_dn(self):
+ """Returns Services DN."""
+ services_dn = self.samdb.get_config_basedn()
+ services_dn.add_child("CN=Services")
+ return services_dn
+
+ def get_claim_types_dn(self):
+ """Returns the Claim Types DN."""
+ claim_types_dn = self.get_services_dn()
+ claim_types_dn.add_child("CN=Claim Types,CN=Claims Configuration")
+ return claim_types_dn
+
+ @classmethod
+ def _run(cls, *argv):
+ """Override _run, so we don't always have to pass host and creds."""
+ args = list(argv)
+ args.extend(["-H", HOST, CREDS])
+ return super()._run(*args)
+
+ runcmd = _run
+ runsubcmd = _run
+
+ @classmethod
+ def create_claim_type(cls, attribute, name=None, description=None,
+ classes=None, disable=False, protect=False):
+ """Create a claim type using the samba-tool command."""
+
+ # if name is specified it will override the attribute name
+ display_name = name or attribute
+
+ # base command for create claim-type
+ cmd = ["domain", "claim", "claim-type",
+ "create", "--attribute", attribute]
+
+ # list of classes (applies_to)
+ if classes is not None:
+ cmd.extend([f"--class={name}" for name in classes])
+
+ # optional attributes
+ if name is not None:
+ cmd.append(f"--name={name}")
+ if description is not None:
+ cmd.append(f"--description={description}")
+ if disable:
+ cmd.append("--disable")
+ if protect:
+ cmd.append("--protect")
+
+ result, out, err = cls.runcmd(*cmd)
+ assert result is None
+ assert out.startswith("Created claim type")
+ cls.addClassCleanup(cls.delete_claim_type, name=display_name, force=True)
+ return display_name
+
+ @classmethod
+ def delete_claim_type(cls, name, force=False):
+ """Delete claim type by display name."""
+ cmd = ["domain", "claim", "claim-type", "delete", "--name", name]
+
+ # Force-delete protected claim type.
+ if force:
+ cmd.append("--force")
+
+ result, out, err = cls.runcmd(*cmd)
+ assert result is None
+ assert "Deleted claim type" in out
+
+ def get_claim_type(self, name):
+ """Get claim type by display name."""
+ claim_types_dn = self.get_claim_types_dn()
+
+ result = self.samdb.search(base=claim_types_dn,
+ scope=SCOPE_ONELEVEL,
+ expression=f"(displayName={name})")
+
+ if len(result) == 1:
+ return result[0]
+
+
+class ClaimTypeCmdTestCase(BaseClaimCmdTest):
+ """Tests for the claim-type command."""
+
+ def test_list(self):
+ """Test listing claim types in list format."""
+ result, out, err = self.runcmd("domain", "claim", "claim-type", "list")
+ self.assertIsNone(result, msg=err)
+
+ expected_claim_types = ["expires", "dept", "plate"]
+
+ for claim_type in expected_claim_types:
+ self.assertIn(claim_type, out)
+
+ def test_list__json(self):
+ """Test listing claim types in JSON format."""
+ result, out, err = self.runcmd("domain", "claim", "claim-type",
+ "list", "--json")
+ self.assertIsNone(result, msg=err)
+
+ # we should get valid json
+ json_result = json.loads(out)
+ claim_types = list(json_result.keys())
+
+ expected_claim_types = ["expires", "dept", "plate"]
+
+ for claim_type in expected_claim_types:
+ self.assertIn(claim_type, claim_types)
+
+ def test_view(self):
+ """Test viewing a single claim type."""
+ result, out, err = self.runcmd("domain", "claim", "claim-type",
+ "view", "--name", "expires")
+ self.assertIsNone(result, msg=err)
+
+ # we should get valid json
+ claim_type = json.loads(out)
+
+ # check a few fields only
+ self.assertEqual(claim_type["displayName"], "expires")
+ self.assertEqual(claim_type["description"], "Account-Expires")
+
+ def test_view__name_missing(self):
+ """Test view claim type without --name is handled."""
+ result, out, err = self.runcmd("domain", "claim", "claim-type", "view")
+ self.assertEqual(result, -1)
+ self.assertIn("Argument --name is required.", err)
+
+ def test_view__notfound(self):
+ """Test viewing claim type that doesn't exist is handled."""
+ result, out, err = self.runcmd("domain", "claim", "claim-type",
+ "view", "--name", "doesNotExist")
+ self.assertEqual(result, -1)
+ self.assertIn("Claim type doesNotExist not found.", err)
+
+ def test_create(self):
+ """Test creating several known attributes as claim types.
+
+ The point is to test it against the various datatypes that could
+ be found, but not include every known attribute.
+ """
+ # We just need to test a few different data types for attributes,
+ # there is no need to test every known attribute.
+ claim_types = [
+ "adminCount",
+ "accountExpires",
+ "department",
+ "carLicense",
+ "msDS-PrimaryComputer",
+ "isDeleted",
+ ]
+
+ # Each known attribute must be in the schema.
+ for attribute in claim_types:
+ # Use a different name, so we don't clash with existing attributes.
+ name = "test_create_" + attribute
+
+ self.addCleanup(self.delete_claim_type, name=name, force=True)
+
+ result, out, err = self.runcmd("domain", "claim", "claim-type",
+ "create",
+ "--attribute", attribute,
+ "--name", name,
+ "--class=user")
+ self.assertIsNone(result, msg=err)
+
+ # It should have used the attribute name as displayName.
+ claim_type = self.get_claim_type(name)
+ self.assertEqual(str(claim_type["displayName"]), name)
+ self.assertEqual(str(claim_type["Enabled"]), "TRUE")
+ self.assertEqual(str(claim_type["objectClass"][-1]), "msDS-ClaimType")
+ self.assertEqual(str(claim_type["msDS-ClaimSourceType"]), "AD")
+
+ def test_create__boolean(self):
+ """Test adding a known boolean attribute and check its type."""
+ self.addCleanup(self.delete_claim_type, name="boolAttr", force=True)
+
+ result, out, err = self.runcmd("domain", "claim", "claim-type",
+ "create", "--attribute=msNPAllowDialin",
+ "--name=boolAttr", "--class=user")
+
+ self.assertIsNone(result, msg=err)
+ claim_type = self.get_claim_type("boolAttr")
+ self.assertEqual(str(claim_type["displayName"]), "boolAttr")
+ self.assertEqual(str(claim_type["msDS-ClaimValueType"]), "6")
+
+ def test_create__number(self):
+ """Test adding a known numeric attribute and check its type."""
+ self.addCleanup(self.delete_claim_type, name="intAttr", force=True)
+
+ result, out, err = self.runcmd("domain", "claim", "claim-type",
+ "create", "--attribute=adminCount",
+ "--name=intAttr", "--class=user")
+
+ self.assertIsNone(result, msg=err)
+ claim_type = self.get_claim_type("intAttr")
+ self.assertEqual(str(claim_type["displayName"]), "intAttr")
+ self.assertEqual(str(claim_type["msDS-ClaimValueType"]), "1")
+
+ def test_create__text(self):
+ """Test adding a known text attribute and check its type."""
+ self.addCleanup(self.delete_claim_type, name="textAttr", force=True)
+
+ result, out, err = self.runcmd("domain", "claim", "claim-type",
+ "create", "--attribute=givenName",
+ "--name=textAttr", "--class=user")
+
+ self.assertIsNone(result, msg=err)
+ claim_type = self.get_claim_type("textAttr")
+ self.assertEqual(str(claim_type["displayName"]), "textAttr")
+ self.assertEqual(str(claim_type["msDS-ClaimValueType"]), "3")
+
+ def test_create__disabled(self):
+ """Test adding a disabled attribute."""
+ self.addCleanup(self.delete_claim_type, name="disabledAttr", force=True)
+
+ result, out, err = self.runcmd("domain", "claim", "claim-type",
+ "create", "--attribute=msTSHomeDrive",
+ "--name=disabledAttr", "--class=user",
+ "--disable")
+
+ self.assertIsNone(result, msg=err)
+ claim_type = self.get_claim_type("disabledAttr")
+ self.assertEqual(str(claim_type["displayName"]), "disabledAttr")
+ self.assertEqual(str(claim_type["Enabled"]), "FALSE")
+
+ def test_create__protected(self):
+ """Test adding a protected attribute."""
+ self.addCleanup(self.delete_claim_type, name="protectedAttr", force=True)
+
+ result, out, err = self.runcmd("domain", "claim", "claim-type",
+ "create", "--attribute=mobile",
+ "--name=protectedAttr", "--class=user",
+ "--protect")
+
+ self.assertIsNone(result, msg=err)
+ claim_type = self.get_claim_type("protectedAttr")
+ self.assertEqual(str(claim_type["displayName"]), "protectedAttr")
+
+ # Check if the claim type is protected from accidental deletion.
+ utils = SDUtils(self.samdb)
+ desc = utils.get_sd_as_sddl(claim_type["dn"])
+ self.assertIn("(D;;DTSD;;;WD)", desc)
+
+ def test_create__classes(self):
+ """Test adding an attribute applied to different classes."""
+ schema_dn = self.samdb.get_schema_basedn()
+ user_dn = f"CN=User,{schema_dn}"
+ computer_dn = f"CN=Computer,{schema_dn}"
+
+ # --class=user
+ self.addCleanup(self.delete_claim_type, name="streetName", force=True)
+ result, out, err = self.runcmd("domain", "claim", "claim-type",
+ "create", "--attribute=street",
+ "--name=streetName", "--class=user")
+ self.assertIsNone(result, msg=err)
+ claim_type = self.get_claim_type("streetName")
+ applies_to = [str(dn) for dn in claim_type["msDS-ClaimTypeAppliesToClass"]]
+ self.assertEqual(str(claim_type["displayName"]), "streetName")
+ self.assertEqual(len(applies_to), 1)
+ self.assertIn(user_dn, applies_to)
+ self.assertNotIn(computer_dn, applies_to)
+
+ # --class=computer
+ self.addCleanup(self.delete_claim_type, name="ext", force=True)
+ result, out, err = self.runcmd("domain", "claim", "claim-type",
+ "create", "--attribute=extensionName",
+ "--name=ext", "--class=computer")
+ self.assertIsNone(result, msg=err)
+ claim_type = self.get_claim_type("ext")
+ applies_to = [str(dn) for dn in claim_type["msDS-ClaimTypeAppliesToClass"]]
+ self.assertEqual(str(claim_type["displayName"]), "ext")
+ self.assertEqual(len(applies_to), 1)
+ self.assertNotIn(user_dn, applies_to)
+ self.assertIn(computer_dn, applies_to)
+
+ # --class=user --class=computer
+ self.addCleanup(self.delete_claim_type,
+ name="primaryComputer", force=True)
+ result, out, err = self.runcmd("domain", "claim", "claim-type",
+ "create", "--attribute=msDS-PrimaryComputer",
+ "--name=primaryComputer", "--class=user",
+ "--class=computer")
+ self.assertIsNone(result, msg=err)
+ claim_type = self.get_claim_type("primaryComputer")
+ applies_to = [str(dn) for dn in claim_type["msDS-ClaimTypeAppliesToClass"]]
+ self.assertEqual(str(claim_type["displayName"]), "primaryComputer")
+ self.assertEqual(len(applies_to), 2)
+ self.assertIn(user_dn, applies_to)
+ self.assertIn(computer_dn, applies_to)
+
+ # No classes should raise CommandError.
+ result, out, err = self.runcmd("domain", "claim", "claim-type",
+ "create", "--attribute=wWWHomePage",
+ "--name=homepage")
+ self.assertEqual(result, -1)
+ self.assertIn("Argument --class is required.", err)
+
+ def test__delete(self):
+ """Test deleting a claim type that is not protected."""
+ # Create non-protected claim type.
+ result, out, err = self.runcmd("domain", "claim", "claim-type",
+ "create", "--attribute=msDS-SiteName",
+ "--name=siteName", "--class=computer")
+ self.assertIsNone(result, msg=err)
+ claim_type = self.get_claim_type("siteName")
+ self.assertIsNotNone(claim_type)
+
+ # Do the deletion.
+ result, out, err = self.runcmd("domain", "claim", "claim-type",
+ "delete", "--name=siteName")
+ self.assertIsNone(result, msg=err)
+
+ # Claim type shouldn't exist anymore.
+ claim_type = self.get_claim_type("siteName")
+ self.assertIsNone(claim_type)
+
+ def test_delete__protected(self):
+ """Test deleting a protected claim type, with and without --force."""
+ # Create protected claim type.
+ result, out, err = self.runcmd("domain", "claim", "claim-type",
+ "create", "--attribute=postalCode",
+ "--name=postcode", "--class=user",
+ "--protect")
+ self.assertIsNone(result, msg=err)
+ claim_type = self.get_claim_type("postcode")
+ self.assertIsNotNone(claim_type)
+
+ # Do the deletion.
+ result, out, err = self.runcmd("domain", "claim", "claim-type",
+ "delete", "--name=postcode")
+ self.assertEqual(result, -1)
+
+ # Claim type should still exist.
+ claim_type = self.get_claim_type("postcode")
+ self.assertIsNotNone(claim_type)
+
+ # Try a force delete instead.
+ result, out, err = self.runcmd("domain", "claim", "claim-type",
+ "delete", "--name=postcode", "--force")
+ self.assertIsNone(result, msg=err)
+
+ # Claim type shouldn't exist anymore.
+ claim_type = self.get_claim_type("siteName")
+ self.assertIsNone(claim_type)
+
+ def test_delete__notfound(self):
+ """Test deleting a claim type that doesn't exist."""
+ result, out, err = self.runcmd("domain", "claim", "claim-type",
+ "delete", "--name", "doesNotExist")
+ self.assertEqual(result, -1)
+ self.assertIn("Claim type doesNotExist not found.", err)
+
+ def test_modify__description(self):
+ """Test modifying a claim type description."""
+ self.addCleanup(self.delete_claim_type, name="company", force=True)
+ self.create_claim_type("company", classes=["user"])
+
+ result, out, err = self.runcmd("domain", "claim", "claim-type",
+ "modify", "--name", "company",
+ "--description=NewDescription")
+ self.assertIsNone(result, msg=err)
+
+ # Verify fields were changed.
+ claim_type = self.get_claim_type("company")
+ self.assertEqual(str(claim_type["description"]), "NewDescription")
+
+ def test_modify__classes(self):
+ """Test modify claim type classes."""
+ schema_dn = self.samdb.get_schema_basedn()
+ user_dn = f"CN=User,{schema_dn}"
+ computer_dn = f"CN=Computer,{schema_dn}"
+
+ self.addCleanup(self.delete_claim_type, name="seeAlso", force=True)
+ self.create_claim_type("seeAlso", classes=["user"])
+
+ # First try removing all classes which shouldn't be allowed.
+ result, out, err = self.runcmd("domain", "claim", "claim-type",
+ "modify", "--name", "seeAlso",
+ "--class=")
+ self.assertEqual(result, -1)
+ self.assertIn("Class name is required.", err)
+
+ # Try changing it to just --class=computer first.
+ result, out, err = self.runcmd("domain", "claim", "claim-type",
+ "modify", "--name", "seeAlso",
+ "--class=computer")
+ self.assertIsNone(result, msg=err)
+ claim_type = self.get_claim_type("seeAlso")
+ applies_to = [str(dn) for dn in claim_type["msDS-ClaimTypeAppliesToClass"]]
+ self.assertNotIn(user_dn, applies_to)
+ self.assertIn(computer_dn, applies_to)
+
+ # Now try changing it to --class=user again.
+ result, out, err = self.runcmd("domain", "claim", "claim-type",
+ "modify", "--name", "seeAlso",
+ "--class=user")
+ self.assertIsNone(result, msg=err)
+ claim_type = self.get_claim_type("seeAlso")
+ applies_to = [str(dn) for dn in claim_type["msDS-ClaimTypeAppliesToClass"]]
+ self.assertIn(user_dn, applies_to)
+ self.assertNotIn(computer_dn, applies_to)
+
+ # Why not both?
+ result, out, err = self.runcmd("domain", "claim", "claim-type",
+ "modify", "--name", "seeAlso",
+ "--class=user", "--class=computer")
+ self.assertIsNone(result, msg=err)
+ claim_type = self.get_claim_type("seeAlso")
+ applies_to = [str(dn) for dn in claim_type["msDS-ClaimTypeAppliesToClass"]]
+ self.assertIn(user_dn, applies_to)
+ self.assertIn(computer_dn, applies_to)
+
+ def test_modify__enable_disable(self):
+ """Test modify disabling and enabling a claim type."""
+ self.addCleanup(self.delete_claim_type, name="catalogs", force=True)
+ self.create_claim_type("catalogs", classes=["user"])
+
+ result, out, err = self.runcmd("domain", "claim", "claim-type",
+ "modify", "--name", "catalogs",
+ "--disable")
+ self.assertIsNone(result, msg=err)
+
+ # Check that claim type was disabled.
+ claim_type = self.get_claim_type("catalogs")
+ self.assertEqual(str(claim_type["Enabled"]), "FALSE")
+
+ result, out, err = self.runcmd("domain", "claim", "claim-type",
+ "modify", "--name", "catalogs",
+ "--enable")
+ self.assertIsNone(result, msg=err)
+
+ # Check that claim type was enabled.
+ claim_type = self.get_claim_type("catalogs")
+ self.assertEqual(str(claim_type["Enabled"]), "TRUE")
+
+ def test_modify__protect_unprotect(self):
+ """Test modify un-protecting and protecting a claim type."""
+ self.addCleanup(self.delete_claim_type, name="pager", force=True)
+ self.create_claim_type("pager", classes=["user"])
+
+ utils = SDUtils(self.samdb)
+ result, out, err = self.runcmd("domain", "claim", "claim-type",
+ "modify", "--name", "pager",
+ "--protect")
+ self.assertIsNone(result, msg=err)
+
+ # Check that claim type was protected.
+ claim_type = self.get_claim_type("pager")
+ desc = utils.get_sd_as_sddl(claim_type["dn"])
+ self.assertIn("(D;;DTSD;;;WD)", desc)
+
+ result, out, err = self.runcmd("domain", "claim", "claim-type",
+ "modify", "--name", "pager",
+ "--unprotect")
+ self.assertIsNone(result, msg=err)
+
+ # Check that claim type was unprotected.
+ claim_type = self.get_claim_type("pager")
+ desc = utils.get_sd_as_sddl(claim_type["dn"])
+ self.assertNotIn("(D;;DTSD;;;WD)", desc)
+
+ def test_modify__enable_disable_together(self):
+ """Test modify claim type doesn't allow both --enable and --disable."""
+ self.addCleanup(self.delete_claim_type,
+ name="businessCategory", force=True)
+ self.create_claim_type("businessCategory", classes=["user"])
+
+ result, out, err = self.runcmd("domain", "claim", "claim-type",
+ "modify", "--name", "businessCategory",
+ "--enable", "--disable")
+ self.assertEqual(result, -1)
+ self.assertIn("--enable and --disable cannot be used together.", err)
+
+ def test_modify__protect_unprotect_together(self):
+ """Test modify claim type using both --protect and --unprotect."""
+ self.addCleanup(self.delete_claim_type,
+ name="businessCategory", force=True)
+ self.create_claim_type("businessCategory", classes=["user"])
+
+ result, out, err = self.runcmd("domain", "claim", "claim-type",
+ "modify", "--name", "businessCategory",
+ "--protect", "--unprotect")
+ self.assertEqual(result, -1)
+ self.assertIn("--protect and --unprotect cannot be used together.", err)
+
+ def test_modify__notfound(self):
+ """Test modify a claim type that doesn't exist."""
+ result, out, err = self.runcmd("domain", "claim", "claim-type",
+ "modify", "--name", "doesNotExist",
+ "--description=NewDescription")
+ self.assertEqual(result, -1)
+ self.assertIn("Claim type doesNotExist not found.", err)
+
+
+class ValueTypeCmdTestCase(BaseClaimCmdTest):
+ """Tests for the value-type command."""
+
+ def test_list(self):
+ """Test listing claim value types in list format."""
+ result, out, err = self.runcmd("domain", "claim", "value-type", "list")
+ self.assertIsNone(result, msg=err)
+
+ # base list of value types is there
+ for value_type in VALUE_TYPES:
+ self.assertIn(value_type, out)
+
+ def test_list__json(self):
+ """Test listing claim value types in JSON format."""
+ result, out, err = self.runcmd("domain", "claim", "value-type",
+ "list", "--json")
+ self.assertIsNone(result, msg=err)
+
+ # we should get valid json
+ json_result = json.loads(out)
+ value_types = list(json_result.keys())
+
+ # base list of value types is there
+ for value_type in VALUE_TYPES:
+ self.assertIn(value_type, value_types)
+
+ def test_view(self):
+ """Test viewing a single claim value type."""
+ result, out, err = self.runcmd("domain", "claim", "value-type",
+ "view", "--name", "Text")
+ self.assertIsNone(result, msg=err)
+
+ # we should get valid json
+ value_type = json.loads(out)
+
+ # check a few fields only
+ self.assertEqual(value_type["name"], "MS-DS-Text")
+ self.assertEqual(value_type["displayName"], "Text")
+ self.assertEqual(value_type["msDS-ClaimValueType"], 3)
+
+ def test_view__name_missing(self):
+ """Test viewing a claim value type with missing --name is handled."""
+ result, out, err = self.runcmd("domain", "claim", "value-type", "view")
+ self.assertEqual(result, -1)
+ self.assertIn("Argument --name is required.", err)
+
+ def test_view__notfound(self):
+ """Test viewing a claim value type that doesn't exist is handled."""
+ result, out, err = self.runcmd("domain", "claim", "value-type",
+ "view", "--name", "doesNotExist")
+ self.assertEqual(result, -1)
+ self.assertIn("Value type doesNotExist not found.", err)
diff --git a/python/samba/tests/samba_tool/domain_models.py b/python/samba/tests/samba_tool/domain_models.py
new file mode 100644
index 0000000..e0f21fe
--- /dev/null
+++ b/python/samba/tests/samba_tool/domain_models.py
@@ -0,0 +1,416 @@
+# Unix SMB/CIFS implementation.
+#
+# Tests for domain models and fields
+#
+# Copyright (C) Catalyst.Net Ltd. 2023
+#
+# Written by Rob van der Linde <rob@catalyst.net.nz>
+#
+# 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 os
+from datetime import datetime
+from xml.etree import ElementTree
+
+from ldb import FLAG_MOD_ADD, MessageElement, SCOPE_ONELEVEL
+from samba.dcerpc import security
+from samba.dcerpc.misc import GUID
+from samba.netcmd.domain.models import Group, User, fields
+from samba.netcmd.domain.models.auth_policy import StrongNTLMPolicy
+from samba.ndr import ndr_pack, ndr_unpack
+
+from .base import SambaToolCmdTest
+
+HOST = "ldap://{DC_SERVER}".format(**os.environ)
+CREDS = "-U{DC_USERNAME}%{DC_PASSWORD}".format(**os.environ)
+
+
+class FieldTestMixin:
+ """Tests a model field to ensure it behaves correctly in both directions.
+
+ Use a mixin since TestCase can't be marked as abstract.
+ """
+
+ @classmethod
+ def setUpClass(cls):
+ cls.samdb = cls.getSamDB("-H", HOST, CREDS)
+ super().setUpClass()
+
+ def get_users_dn(self):
+ """Returns Users DN."""
+ users_dn = self.samdb.get_root_basedn()
+ users_dn.add_child("CN=Users")
+ return users_dn
+
+ def test_to_db_value(self):
+ # Loop through each value and expected value combination.
+ # If the expected value is callable, treat it as a validation callback.
+ # NOTE: perhaps we should be using subtests for this.
+ for (value, expected) in self.to_db_value:
+ db_value = self.field.to_db_value(self.samdb, value, FLAG_MOD_ADD)
+ if callable(expected):
+ self.assertTrue(expected(db_value))
+ else:
+ self.assertEqual(db_value, expected)
+
+ def test_from_db_value(self):
+ # Loop through each value and expected value combination.
+ # NOTE: perhaps we should be using subtests for this.
+ for (db_value, expected) in self.from_db_value:
+ value = self.field.from_db_value(self.samdb, db_value)
+ self.assertEqual(value, expected)
+
+
+class IntegerFieldTest(FieldTestMixin, SambaToolCmdTest):
+ field = fields.IntegerField("FieldName")
+
+ to_db_value = [
+ (10, MessageElement(b"10")),
+ ([1, 5, 10], MessageElement([b"1", b"5", b"10"])),
+ (None, None),
+ ]
+
+ from_db_value = [
+ (MessageElement(b"10"), 10),
+ (MessageElement([b"1", b"5", b"10"]), [1, 5, 10]),
+ (None, None),
+ ]
+
+
+class BinaryFieldTest(FieldTestMixin, SambaToolCmdTest):
+ field = fields.BinaryField("FieldName")
+
+ to_db_value = [
+ (b"SAMBA", MessageElement(b"SAMBA")),
+ ([b"SAMBA", b"Developer"], MessageElement([b"SAMBA", b"Developer"])),
+ (None, None),
+ ]
+
+ from_db_value = [
+ (MessageElement(b"SAMBA"), b"SAMBA"),
+ (MessageElement([b"SAMBA", b"Developer"]), [b"SAMBA", b"Developer"]),
+ (None, None),
+ ]
+
+
+class StringFieldTest(FieldTestMixin, SambaToolCmdTest):
+ field = fields.StringField("FieldName")
+
+ to_db_value = [
+ ("SAMBA", MessageElement(b"SAMBA")),
+ (["SAMBA", "Developer"], MessageElement([b"SAMBA", b"Developer"])),
+ (None, None),
+ ]
+
+ from_db_value = [
+ (MessageElement(b"SAMBA"), "SAMBA"),
+ (MessageElement([b"SAMBA", b"Developer"]), ["SAMBA", "Developer"]),
+ (None, None),
+ ]
+
+
+class BooleanFieldTest(FieldTestMixin, SambaToolCmdTest):
+ field = fields.BooleanField("FieldName")
+
+ to_db_value = [
+ (True, MessageElement(b"TRUE")),
+ ([False, True], MessageElement([b"FALSE", b"TRUE"])),
+ (None, None),
+ ]
+
+ from_db_value = [
+ (MessageElement(b"TRUE"), True),
+ (MessageElement([b"FALSE", b"TRUE"]), [False, True]),
+ (None, None),
+ ]
+
+
+class EnumFieldTest(FieldTestMixin, SambaToolCmdTest):
+ field = fields.EnumField("FieldName", StrongNTLMPolicy)
+
+ to_db_value = [
+ (StrongNTLMPolicy.OPTIONAL, MessageElement("1")),
+ ([StrongNTLMPolicy.REQUIRED, StrongNTLMPolicy.OPTIONAL],
+ MessageElement(["2", "1"])),
+ (None, None),
+ ]
+
+ from_db_value = [
+ (MessageElement("1"), StrongNTLMPolicy.OPTIONAL),
+ (MessageElement(["2", "1"]),
+ [StrongNTLMPolicy.REQUIRED, StrongNTLMPolicy.OPTIONAL]),
+ (None, None),
+ ]
+
+
+class DateTimeFieldTest(FieldTestMixin, SambaToolCmdTest):
+ field = fields.DateTimeField("FieldName")
+
+ to_db_value = [
+ (datetime(2023, 1, 27, 22, 36, 41), MessageElement("20230127223641.0Z")),
+ ([datetime(2023, 1, 27, 22, 36, 41), datetime(2023, 1, 27, 22, 47, 50)],
+ MessageElement(["20230127223641.0Z", "20230127224750.0Z"])),
+ (None, None),
+ ]
+
+ from_db_value = [
+ (MessageElement("20230127223641.0Z"), datetime(2023, 1, 27, 22, 36, 41)),
+ (MessageElement(["20230127223641.0Z", "20230127224750.0Z"]),
+ [datetime(2023, 1, 27, 22, 36, 41), datetime(2023, 1, 27, 22, 47, 50)]),
+ (None, None),
+ ]
+
+
+class RelatedFieldTest(FieldTestMixin, SambaToolCmdTest):
+ field = fields.RelatedField("FieldName", User)
+
+ @property
+ def to_db_value(self):
+ alice = User.get(self.samdb, username="alice")
+ joe = User.get(self.samdb, username="joe")
+ return [
+ (alice, MessageElement(str(alice.dn))),
+ ([joe, alice], MessageElement([str(joe.dn), str(alice.dn)])),
+ (None, None),
+ ]
+
+ @property
+ def from_db_value(self):
+ alice = User.get(self.samdb, username="alice")
+ joe = User.get(self.samdb, username="joe")
+ return [
+ (MessageElement(str(alice.dn)), alice),
+ (MessageElement([str(joe.dn), str(alice.dn)]), [joe, alice]),
+ (None, None),
+ ]
+
+
+class DnFieldTest(FieldTestMixin, SambaToolCmdTest):
+ field = fields.DnField("FieldName")
+
+ @property
+ def to_db_value(self):
+ alice = User.get(self.samdb, username="alice")
+ joe = User.get(self.samdb, username="joe")
+ return [
+ (alice.dn, MessageElement(str(alice.dn))),
+ ([joe.dn, alice.dn], MessageElement([str(joe.dn), str(alice.dn)])),
+ (None, None),
+ ]
+
+ @property
+ def from_db_value(self):
+ alice = User.get(self.samdb, username="alice")
+ joe = User.get(self.samdb, username="joe")
+ return [
+ (MessageElement(str(alice.dn)), alice.dn),
+ (MessageElement([str(joe.dn), str(alice.dn)]), [joe.dn, alice.dn]),
+ (None, None),
+ ]
+
+
+class SIDFieldTest(FieldTestMixin, SambaToolCmdTest):
+ field = fields.SIDField("FieldName")
+
+ @property
+ def to_db_value(self):
+ # Create a group for testing
+ group = Group(name="group1")
+ group.save(self.samdb)
+ self.addCleanup(group.delete, self.samdb)
+
+ # Get raw value to compare against
+ group_rec = self.samdb.search(Group.get_base_dn(self.samdb),
+ scope=SCOPE_ONELEVEL,
+ expression="(name=group1)",
+ attrs=["objectSid"])[0]
+ raw_sid = group_rec["objectSid"]
+
+ return [
+ (group.object_sid, raw_sid),
+ (None, None),
+ ]
+
+ @property
+ def from_db_value(self):
+ # Create a group for testing
+ group = Group(name="group1")
+ group.save(self.samdb)
+ self.addCleanup(group.delete, self.samdb)
+
+ # Get raw value to compare against
+ group_rec = self.samdb.search(Group.get_base_dn(self.samdb),
+ scope=SCOPE_ONELEVEL,
+ expression="(name=group1)",
+ attrs=["objectSid"])[0]
+ raw_sid = group_rec["objectSid"]
+
+ return [
+ (raw_sid, group.object_sid),
+ (None, None),
+ ]
+
+
+class GUIDFieldTest(FieldTestMixin, SambaToolCmdTest):
+ field = fields.GUIDField("FieldName")
+
+ @property
+ def to_db_value(self):
+ users_dn = self.get_users_dn()
+
+ alice = self.samdb.search(users_dn,
+ scope=SCOPE_ONELEVEL,
+ expression="(sAMAccountName=alice)",
+ attrs=["objectGUID"])[0]
+
+ joe = self.samdb.search(users_dn,
+ scope=SCOPE_ONELEVEL,
+ expression="(sAMAccountName=joe)",
+ attrs=["objectGUID"])[0]
+
+ alice_guid = str(ndr_unpack(GUID, alice["objectGUID"][0]))
+ joe_guid = str(ndr_unpack(GUID, joe["objectGUID"][0]))
+
+ return [
+ (alice_guid, alice["objectGUID"]),
+ (
+ [joe_guid, alice_guid],
+ MessageElement([joe["objectGUID"][0], alice["objectGUID"][0]]),
+ ),
+ (None, None),
+ ]
+
+ @property
+ def from_db_value(self):
+ users_dn = self.get_users_dn()
+
+ alice = self.samdb.search(users_dn,
+ scope=SCOPE_ONELEVEL,
+ expression="(sAMAccountName=alice)",
+ attrs=["objectGUID"])[0]
+
+ joe = self.samdb.search(users_dn,
+ scope=SCOPE_ONELEVEL,
+ expression="(sAMAccountName=joe)",
+ attrs=["objectGUID"])[0]
+
+ alice_guid = str(ndr_unpack(GUID, alice["objectGUID"][0]))
+ joe_guid = str(ndr_unpack(GUID, joe["objectGUID"][0]))
+
+ return [
+ (alice["objectGUID"], alice_guid),
+ (
+ MessageElement([joe["objectGUID"][0], alice["objectGUID"][0]]),
+ [joe_guid, alice_guid],
+ ),
+ (None, None),
+ ]
+
+
+class SDDLFieldTest(FieldTestMixin, SambaToolCmdTest):
+ field = fields.SDDLField("FieldName")
+
+ def setUp(self):
+ super().setUp()
+ self.domain_sid = security.dom_sid(self.samdb.get_domain_sid())
+
+ def encode(self, value):
+ return ndr_pack(security.descriptor.from_sddl(value, self.domain_sid))
+
+ @property
+ def to_db_value(self):
+ values = [
+ "O:SYG:SYD:(XA;OICI;CR;;;WD;(Member_of {SID(AU)}))",
+ "O:SYG:SYD:(XA;OICI;CR;;;WD;(Member_of {SID(AO)}))",
+ "O:SYG:SYD:(XA;OICI;CR;;;WD;((Member_of {SID(AO)}) || (Member_of {SID(BO)})))",
+ "O:SYG:SYD:(XA;OICI;CR;;;WD;(Member_of {SID(%s)}))" % self.domain_sid,
+ ]
+ expected = [
+ (value, MessageElement(self.encode(value))) for value in values
+ ]
+ expected.append((None, None))
+ return expected
+
+ @property
+ def from_db_value(self):
+ values = [
+ "O:SYG:SYD:(XA;OICI;CR;;;WD;(Member_of {SID(AU)}))",
+ "O:SYG:SYD:(XA;OICI;CR;;;WD;(Member_of {SID(AO)}))",
+ "O:SYG:SYD:(XA;OICI;CR;;;WD;((Member_of {SID(AO)}) || (Member_of {SID(BO)})))",
+ "O:SYG:SYD:(XA;OICI;CR;;;WD;(Member_of {SID(%s)}))" % self.domain_sid,
+ ]
+ expected = [
+ (MessageElement(self.encode(value)), value) for value in values
+ ]
+ expected.append((None, None))
+ return expected
+
+
+class PossibleClaimValuesFieldTest(FieldTestMixin, SambaToolCmdTest):
+ field = fields.PossibleClaimValuesField("FieldName")
+
+ json_data = [{
+ "ValueGUID": "1c39ed4f-0b26-4536-b963-5959c8b1b676",
+ "ValueDisplayName": "Alice",
+ "ValueDescription": "Alice Description",
+ "Value": "alice",
+ }]
+
+ xml_data = "<?xml version='1.0' encoding='utf-16'?>" \
+ "<PossibleClaimValues xmlns:xsd='http://www.w3.org/2001/XMLSchema'" \
+ " xmlns:xsi='http://www.w3.org/2001/XMLSchema-instance'" \
+ " xmlns='http://schemas.microsoft.com/2010/08/ActiveDirectory/PossibleValues'>" \
+ "<StringList>" \
+ "<Item>" \
+ "<ValueGUID>1c39ed4f-0b26-4536-b963-5959c8b1b676</ValueGUID>" \
+ "<ValueDisplayName>Alice</ValueDisplayName>" \
+ "<ValueDescription>Alice Description</ValueDescription>" \
+ "<Value>alice</Value>" \
+ "</Item>" \
+ "</StringList>" \
+ "</PossibleClaimValues>"
+
+ def validate_xml(self, db_field):
+ """Callback that compares XML strings.
+
+ Tidying the HTMl output and adding consistent indentation was only
+ added to ETree in Python 3.9+ so generate a single line XML string.
+
+ This is just based on comparing the parsed XML, converted back
+ to a string, then comparing those strings.
+
+ So the expected xml_data string must have no spacing or indentation.
+
+ :param db_field: MessageElement value returned by field.to_db_field()
+ """
+ expected = ElementTree.fromstring(self.xml_data)
+ parsed = ElementTree.fromstring(str(db_field))
+ return ElementTree.tostring(parsed) == ElementTree.tostring(expected)
+
+ @property
+ def to_db_value(self):
+ return [
+ (self.json_data, self.validate_xml), # callback to validate XML
+ (self.json_data[0], self.validate_xml), # one item wrapped as list
+ ([], None), # empty list clears field
+ (None, None),
+ ]
+
+ @property
+ def from_db_value(self):
+ return [
+ (MessageElement(self.xml_data), self.json_data),
+ (None, None),
+ ]
diff --git a/python/samba/tests/samba_tool/drs_clone_dc_data_lmdb_size.py b/python/samba/tests/samba_tool/drs_clone_dc_data_lmdb_size.py
new file mode 100644
index 0000000..1cb88d3
--- /dev/null
+++ b/python/samba/tests/samba_tool/drs_clone_dc_data_lmdb_size.py
@@ -0,0 +1,119 @@
+# Unix SMB/CIFS implementation.
+# Copyright (C) Catalyst IT Ltd. 2019
+#
+# 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.tests.samba_tool.base import SambaToolCmdTest
+import os
+import shutil
+
+
+class DrsCloneDcDataLmdbSizeTestCase(SambaToolCmdTest):
+ """Test setting of the lmdb map size during drs clone-dc-data"""
+
+ def setUp(self):
+ super().setUp()
+ self.tempsambadir = os.path.join(self.tempdir, "samba")
+ os.mkdir(self.tempsambadir)
+
+ # clone a domain and set the lmdb map size to size
+ #
+ # returns the tuple (ret, stdout, stderr)
+ def clone(self, size=None):
+ command = (
+ "samba-tool " +
+ "drs clone-dc-database " +
+ os.environ["REALM"] + " " +
+ ("-U%s%%%s " % (os.environ["USERNAME"], os.environ["PASSWORD"])) +
+ ("--targetdir=%s " % self.tempsambadir) +
+ "--backend-store=mdb "
+ )
+ if size:
+ command += ("--backend-store-size=%s" % size)
+
+ return self.run_command(command)
+
+ #
+ # Get the lmdb map size for the specified command
+ #
+ # While there is a python lmdb package available we use the lmdb command
+ # line utilities to avoid introducing a dependency.
+ #
+ def get_lmdb_environment_size(self, path):
+ (result, out, err) = self.run_command("mdb_stat -ne %s" % path)
+ if result:
+ self.fail("Unable to run mdb_stat\n")
+ for line in out.split("\n"):
+ line = line.strip()
+ if line.startswith("Map size:"):
+ line = line.replace(" ", "")
+ (label, size) = line.split(":")
+ return int(size)
+
+ #
+ # Check the lmdb files created by provision and ensure that the map size
+ # has been set to size.
+ #
+ # Currently this is all the *.ldb files in private/sam.ldb.d
+ #
+ def check_lmdb_environment_sizes(self, size):
+ directory = os.path.join(self.tempsambadir, "private", "sam.ldb.d")
+ for name in os.listdir(directory):
+ if name.endswith(".ldb"):
+ path = os.path.join(directory, name)
+ s = self.get_lmdb_environment_size(path)
+ if s != size:
+ self.fail("File %s, size=%d larger than %d" %
+ (name, s, size))
+
+ #
+ # Ensure that if --backend-store-size is not specified the default of
+ # 8Gb is used
+ def test_default(self):
+ (result, out, err) = self.clone()
+ self.assertEqual(0, result)
+ self.check_lmdb_environment_sizes(8 * 1024 * 1024 * 1024)
+
+ def test_64Mb(self):
+ (result, out, err) = self.clone("64Mb")
+ self.assertEqual(0, result)
+ self.check_lmdb_environment_sizes(64 * 1024 * 1024)
+
+ def test_no_unit_suffix(self):
+ (result, out, err) = self.run_command(
+ 'samba-tool drs clone-dc-database --backend-store-size "2"')
+ self.assertGreater(result, 0)
+ self.assertRegex(err,
+ r"--backend-store-size invalid suffix ''")
+
+ def test_invalid_unit_suffix(self):
+ (result, out, err) = self.run_command(
+ 'samba-tool drs clone-dc-database --backend-store-size "2 cd"')
+ self.assertGreater(result, 0)
+ self.assertRegex(err,
+ r"--backend-store-size invalid suffix 'cd'")
+
+ def test_non_numeric(self):
+ (result, out, err) = self.run_command(
+ 'samba-tool drs clone-dc-database --backend-store-size "two Gb"')
+ self.assertGreater(result, 0)
+ self.assertRegex(
+ err,
+ r"backend-store-size option requires a numeric value, with an"
+ " optional unit suffix")
+
+ def tearDown(self):
+ super().tearDown()
+ shutil.rmtree(self.tempsambadir)
diff --git a/python/samba/tests/samba_tool/dsacl.py b/python/samba/tests/samba_tool/dsacl.py
new file mode 100644
index 0000000..8ddf37e
--- /dev/null
+++ b/python/samba/tests/samba_tool/dsacl.py
@@ -0,0 +1,211 @@
+# Unix SMB/CIFS implementation.
+# Copyright (C) Martin Kraemer 2019 <mk.maddin@gmail.com>
+#
+# 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 os
+from samba.tests.samba_tool.base import SambaToolCmdTest
+import re
+
+class DSaclSetSddlTestCase(SambaToolCmdTest):
+ """Tests for samba-tool dsacl set --sddl subcommand"""
+ sddl = "(OA;CIIO;RPWP;aaaaaaaa-1111-bbbb-2222-dddddddddddd;33333333-eeee-4444-ffff-555555555555;PS)"
+ sddl_lc = "(OA;CIIO;RPWP;aaaaaaaa-1111-bbbb-2222-dddddddddddd;33333333-eeee-4444-ffff-555555555555;PS)"
+ sddl_uc = "(OA;CIIO;RPWP;AAAAAAAA-1111-BBBB-2222-DDDDDDDDDDDD;33333333-EEEE-4444-FFFF-555555555555;PS)"
+ sddl_sid = "(OA;CIIO;RPWP;aaaaaaaa-1111-bbbb-2222-dddddddddddd;33333333-eeee-4444-ffff-555555555555;S-1-5-10)"
+ sddl_multi = "(OA;CIIO;RPWP;aaaaaaaa-1111-bbbb-2222-dddddddddddd;33333333-eeee-4444-ffff-555555555555;PS)(OA;CIIO;RPWP;cccccccc-9999-ffff-8888-eeeeeeeeeeee;77777777-dddd-6666-bbbb-555555555555;PS)"
+
+ def setUp(self):
+ super().setUp()
+ self.samdb = self.getSamDB("-H", "ldap://%s" % os.environ["DC_SERVER"],"-U%s%%%s" % (os.environ["DC_USERNAME"], os.environ["DC_PASSWORD"]))
+ self.dn="OU=DSaclSetSddlTestCase,%s" % self.samdb.domain_dn()
+ self.samdb.create_ou(self.dn)
+
+ def tearDown(self):
+ super().tearDown()
+ # clean-up the created test ou
+ self.samdb.delete(self.dn)
+
+ def test_sddl(self):
+ """Tests if a sddl string can be added 'the normal way'"""
+ (result, out, err) = self.runsubcmd("dsacl", "set","--objectdn=%s" % self.dn, "--sddl=%s" % self.sddl)
+ self.assertCmdSuccess(result, out, err)
+ self.assertEqual(err, "", "Shouldn't be any error messages")
+ #extract only the two sddl strings from samba-tool output
+ acl_list=re.findall('.*descriptor for.*:\n(.*?)\n',out)
+ self.assertNotEqual(acl_list[0], acl_list[1], "new and old SDDL string differ")
+ self.assertMatch(acl_list[1], self.sddl, "new SDDL string should be contained within second sddl output")
+
+ def test_sddl_set_get(self):
+ """Tests if a sddl string can be added 'the normal way' and the output of 'get' is the same"""
+ (result, out, err) = self.runsubcmd("dsacl", "get",
+ "--objectdn=%s" % self.dn)
+ self.assertCmdSuccess(result, out, err)
+ self.assertEqual(err, "", "Shouldn't be any error messages")
+ #extract only the two sddl strings from samba-tool output
+ acl_list_get=re.findall('^descriptor for.*:\n(.*?)\n', out)
+
+ (result, out, err) = self.runsubcmd("dsacl", "set",
+ "--objectdn=%s" % self.dn,
+ "--sddl=%s" % self.sddl)
+ self.assertCmdSuccess(result, out, err)
+ self.assertEqual(err, "", "Shouldn't be any error messages")
+ #extract only the two sddl strings from samba-tool output
+ acl_list_old=re.findall('old descriptor for.*:\n(.*?)\n', out)
+ self.assertEqual(acl_list_old, acl_list_get,
+ "output of dsacl get should be the same as before set")
+
+ acl_list=re.findall('new descriptor for.*:\n(.*?)\n', out)
+
+ (result, out, err) = self.runsubcmd("dsacl", "get",
+ "--objectdn=%s" % self.dn)
+ self.assertCmdSuccess(result, out, err)
+ self.assertEqual(err, "", "Shouldn't be any error messages")
+ #extract only the two sddl strings from samba-tool output
+ acl_list_get2=re.findall('^descriptor for.*:\n(.*?)\n', out)
+ self.assertEqual(acl_list, acl_list_get2,
+ "output of dsacl get should be the same as after set")
+
+ def test_multisddl(self):
+ """Tests if we can add multiple, different sddl strings at the same time"""
+ (result, out, err) = self.runsubcmd("dsacl", "set","--objectdn=%s" % self.dn, "--sddl=%s" % self.sddl_multi)
+ self.assertCmdSuccess(result, out, err)
+ self.assertEqual(err, "", "Shouldn't be any error messages")
+ #extract only the two sddl strings from samba-tool output
+ acl_list=re.findall('.*descriptor for.*:\n(.*?)\n',out)
+ for ace in re.findall(r'\(.*?\)',self.sddl_multi):
+ self.assertMatch(acl_list[1], ace, "new SDDL string should be contained within second sddl output")
+
+ def test_duplicatesddl(self):
+ """Tests if an already existing sddl string can be added causing duplicate entry"""
+ acl_list = self._double_sddl_check(self.sddl,self.sddl)
+ self.assertEqual(acl_list[0],acl_list[1])
+
+ def test_casesensitivesddl(self):
+ """Tests if an already existing sddl string can be added in different cases causing duplicate entry"""
+ acl_list = self._double_sddl_check(self.sddl_lc,self.sddl_uc)
+ self.assertEqual(acl_list[0],acl_list[1])
+
+ def test_sidsddl(self):
+ """Tests if an already existing sddl string can be added with SID instead of SDDL SIDString causing duplicate entry"""
+ acl_list = self._double_sddl_check(self.sddl,self.sddl_sid)
+ self.assertEqual(acl_list[0],acl_list[1])
+
+ def test_twosddl(self):
+ """Tests if an already existing sddl string can be added by using it twice/in combination with non existing sddl string causing duplicate entry"""
+ acl_list = self._double_sddl_check(self.sddl,self.sddl + self.sddl)
+ self.assertEqual(acl_list[0],acl_list[1])
+
+ def _double_sddl_check(self,sddl1,sddl2):
+ """Adds two sddl strings and checks if there was an ace change after the second adding"""
+ (result, out, err) = self.runsubcmd("dsacl", "set","--objectdn=%s" % self.dn, "--sddl=%s" % sddl1)
+ self.assertCmdSuccess(result, out, err)
+ self.assertEqual(err, "", "Shouldn't be any error messages")
+ acl_list = re.findall('.*descriptor for.*:\n(.*?)\n',out)
+ self.assertMatch(acl_list[1], sddl1, "new SDDL string should be contained within second sddl output - is not")
+ #add sddl2
+ (result, out, err) = self.runsubcmd("dsacl", "set","--objectdn=%s" % self.dn, "--sddl=%s" % sddl2)
+ self.assertCmdSuccess(result, out, err)
+ self.assertEqual(err, "", "Shouldn't be any error messages")
+ acl_list = re.findall('.*descriptor for.*:\n(.*?)\n',out)
+ return acl_list
+
+ def test_add_delete_sddl(self):
+ """Tests if a sddl string can be added 'the normal way', deleted and
+ final state is the same as initial.
+ """
+ (result, out, err) = self.runsubcmd("dsacl", "get",
+ "--objectdn=%s" % self.dn)
+ self.assertCmdSuccess(result, out, err)
+ self.assertEqual(err, "", "Shouldn't be any error messages")
+ # extract only the two sddl strings from samba-tool output
+ acl_list_orig = re.findall('^descriptor for.*:\n(.*?)\n', out)[0]
+
+ (result, out, err) = self.runsubcmd("dsacl", "set",
+ "--objectdn=%s" % self.dn,
+ "--sddl=%s" % self.sddl)
+ self.assertCmdSuccess(result, out, err)
+ self.assertEqual(err, "", "Shouldn't be any error messages")
+ acl_list_added = re.findall('new descriptor for.*:\n(.*?)\n', out)[0]
+ self.assertNotEqual(acl_list_added, acl_list_orig, "After adding the SD should be different.")
+ self.assertMatch(acl_list_added, self.sddl, "The added ACE should be part of the new SD.")
+
+ (result, out, err) = self.runsubcmd("dsacl", "delete",
+ "--objectdn=%s" % self.dn,
+ "--sddl=%s" % self.sddl)
+ self.assertCmdSuccess(result, out, err)
+ self.assertEqual(err, "", "Shouldn't be any error messages")
+ acl_list_final = re.findall('new descriptor for.*:\n(.*?)\n', out)[0]
+ self.assertEqual(acl_list_orig, acl_list_final,
+ "output of dsacl delete should be the same as before adding")
+
+ (result, out, err) = self.runsubcmd("dsacl", "get",
+ "--objectdn=%s" % self.dn)
+ self.assertCmdSuccess(result, out, err)
+ self.assertEqual(err, "", "Shouldn't be any error messages")
+ # extract only the two sddl strings from samba-tool output
+ acl_list_final_get = re.findall('^descriptor for.*:\n(.*?)\n', out)[0]
+ self.assertEqual(acl_list_orig, acl_list_final_get,
+ "output of dsacl get should be the same as after adding and deleting again")
+
+ def test_delete(self):
+ # add sddl_multi first
+ (result, out, err) = self.runsubcmd("dsacl", "set",
+ "--objectdn=%s" % self.dn,
+ "--sddl=%s" % self.sddl_multi)
+
+ self.assertCmdSuccess(result, out, err)
+ self.assertEqual(err, "", "Shouldn't be any error messages")
+ # delete sddl
+ (result, out, err) = self.runsubcmd("dsacl", "delete",
+ "--objectdn=%s" % self.dn,
+ "--sddl=%s" % self.sddl)
+ self.assertCmdSuccess(result, out, err)
+ self.assertEqual(err, "", "Shouldn't be any error messages")
+ acl_list_deleted = re.findall('new descriptor for.*:\n(.*?)\n', out)[0]
+
+ self.assertNotRegex(acl_list_deleted, re.escape(self.sddl))
+ left_sddl = self.sddl_multi.replace(self.sddl, "")
+ self.assertRegex(acl_list_deleted, re.escape(left_sddl))
+
+ def test_delete_twice(self):
+ """Tests if deleting twice the same ACEs returns the expected warning."""
+ # add sddl_multi first
+ (result, out, err) = self.runsubcmd("dsacl", "set",
+ "--objectdn=%s" % self.dn,
+ "--sddl=%s" % self.sddl_multi)
+
+ self.assertCmdSuccess(result, out, err)
+ self.assertEqual(err, "", "Shouldn't be any error messages")
+
+ # delete sddl
+ (result, out, err) = self.runsubcmd("dsacl", "delete",
+ "--objectdn=%s" % self.dn,
+ "--sddl=%s" % self.sddl)
+ self.assertCmdSuccess(result, out, err)
+ self.assertEqual(err, "", "Shouldn't be any error messages")
+
+ # delete sddl_multi
+ (result, out, err) = self.runsubcmd("dsacl", "delete",
+ "--objectdn=%s" % self.dn,
+ "--sddl=%s" % self.sddl_multi)
+ self.assertCmdSuccess(result, out, err)
+ self.assertEqual(err, "", "Shouldn't be any error messages")
+ self.assertRegex(out, "WARNING", "Should throw a warning about deleting non existent ace.")
+ warn = re.findall("WARNING: (.*?)\n", out)[0]
+ left_sddl = self.sddl_multi.replace(self.sddl, "")
+ self.assertRegex(warn, re.escape(self.sddl), "Should point out the non existent ace.")
+ self.assertNotRegex(warn, re.escape(left_sddl),
+ "Should not complain about all aces, since one of them is not deleted twice.")
diff --git a/python/samba/tests/samba_tool/forest.py b/python/samba/tests/samba_tool/forest.py
new file mode 100644
index 0000000..23291ca
--- /dev/null
+++ b/python/samba/tests/samba_tool/forest.py
@@ -0,0 +1,70 @@
+# Unix SMB/CIFS implementation.
+# Copyright (C) William Brown <william@blackhats.net.au> 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 os
+import ldb
+from samba.tests.samba_tool.base import SambaToolCmdTest
+
+
+class ForestCmdTestCase(SambaToolCmdTest):
+ """Tests for samba-tool dsacl subcommands"""
+ samdb = None
+
+ def setUp(self):
+ super().setUp()
+ self.samdb = self.getSamDB("-H", "ldap://%s" % os.environ["DC_SERVER"],
+ "-U%s%%%s" % (os.environ["DC_USERNAME"], os.environ["DC_PASSWORD"]))
+ self.domain_dn = self.samdb.domain_dn()
+
+ def tearDown(self):
+ super().tearDown()
+ # Reset the values we might have changed.
+ ds_dn = "CN=Directory Service,CN=Windows NT,CN=Services,CN=Configuration"
+ m = ldb.Message()
+ m.dn = ldb.Dn(self.samdb, "%s,%s" % (ds_dn, self.domain_dn))
+ m['dsheuristics'] = ldb.MessageElement(
+ '0000000', ldb.FLAG_MOD_REPLACE, 'dsheuristics')
+
+ self.samdb.modify(m)
+
+ def test_display(self):
+ """Tests that we can display forest settings"""
+ (result, out, err) = self.runcmd("forest",
+ "directory_service",
+ "show",
+ "-H", "ldap://%s" % os.environ["DC_SERVER"],
+ "-U%s%%%s" % (os.environ["DC_USERNAME"],
+ os.environ["DC_PASSWORD"]))
+
+ self.assertCmdSuccess(result, out, err)
+ self.assertEqual(err, "", "Shouldn't be any error messages")
+ self.assertIn("dsheuristics: <NO VALUE>", out)
+
+ def test_modify_dsheuristics(self):
+ """Test that we can modify the dsheuristics setting"""
+
+ (result, out, err) = self.runcmd("forest",
+ "directory_service",
+ "dsheuristics",
+ "0000002",
+ "-H", "ldap://%s" % os.environ["DC_SERVER"],
+ "-U%s%%%s" % (os.environ["DC_USERNAME"],
+ os.environ["DC_PASSWORD"]))
+
+ self.assertCmdSuccess(result, out, err)
+ self.assertEqual(err, "", "Shouldn't be any error messages")
+ self.assertIn("set dsheuristics: 0000002", out)
diff --git a/python/samba/tests/samba_tool/fsmo.py b/python/samba/tests/samba_tool/fsmo.py
new file mode 100644
index 0000000..29fe7bf
--- /dev/null
+++ b/python/samba/tests/samba_tool/fsmo.py
@@ -0,0 +1,52 @@
+# Unix SMB/CIFS implementation.
+# Copyright (C) Rowland Penny <rpenny@samba.org> 2016
+#
+# 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 os
+import ldb
+from samba.tests.samba_tool.base import SambaToolCmdTest
+
+
+class FsmoCmdTestCase(SambaToolCmdTest):
+ """Test for samba-tool fsmo show subcommand"""
+
+ def test_fsmoget(self):
+ """Run fsmo show to see if it errors"""
+ (result, out, err) = self.runsubcmd("fsmo", "show")
+
+ self.assertCmdSuccess(result, out, err)
+ self.assertEqual(err, "", "Shouldn't be any error messages")
+
+ # Check that the output is sensible
+ samdb = self.getSamDB("-H", "ldap://%s" % os.environ["SERVER"],
+ "-U%s%%%s" % (os.environ["USERNAME"], os.environ["PASSWORD"]))
+
+ try:
+ res = samdb.search(base=ldb.Dn(samdb, "CN=Infrastructure,DC=DomainDnsZones") + samdb.get_default_basedn(),
+ scope=ldb.SCOPE_BASE, attrs=["fsmoRoleOwner"])
+
+ self.assertTrue("DomainDnsZonesMasterRole owner: " + str(res[0]["fsmoRoleOwner"][0]) in out)
+ except ldb.LdbError as e:
+ (enum, string) = e.args
+ if enum == ldb.ERR_NO_SUCH_OBJECT:
+ self.assertTrue("The 'domaindns' role is not present in this domain" in out)
+ else:
+ raise
+
+ res = samdb.search(base=samdb.get_default_basedn(),
+ scope=ldb.SCOPE_BASE, attrs=["fsmoRoleOwner"])
+
+ self.assertTrue("DomainNamingMasterRole owner: " + str(res[0]["fsmoRoleOwner"][0]) in out)
diff --git a/python/samba/tests/samba_tool/gpo.py b/python/samba/tests/samba_tool/gpo.py
new file mode 100644
index 0000000..851c70e
--- /dev/null
+++ b/python/samba/tests/samba_tool/gpo.py
@@ -0,0 +1,1847 @@
+# Unix SMB/CIFS implementation.
+# Copyright (C) Andrew Bartlett 2012
+#
+# based on time.py:
+# Copyright (C) Sean Dague <sdague@linux.vnet.ibm.com> 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/>.
+#
+
+import os, pwd, grp
+import ldb
+import samba
+from samba.tests.samba_tool.base import SambaToolCmdTest
+import shutil
+from samba.netcmd.gpo import get_gpo_dn, get_gpo_info
+from samba.param import LoadParm
+from samba.tests.gpo import stage_file, unstage_file
+from samba.dcerpc import preg
+from samba.ndr import ndr_pack, ndr_unpack
+from samba.common import get_string
+from configparser import ConfigParser
+import xml.etree.ElementTree as etree
+from tempfile import NamedTemporaryFile
+import re
+from samba.gp.gpclass import check_guid
+from samba.gp_parse.gp_ini import GPTIniParser
+
+gpo_load_json = \
+b"""
+[
+ {
+ "keyname": "Software\\\\Policies\\\\Mozilla\\\\Firefox\\\\Homepage",
+ "valuename": "StartPage",
+ "class": "USER",
+ "type": "REG_SZ",
+ "data": "homepage"
+ },
+ {
+ "keyname": "Software\\\\Policies\\\\Mozilla\\\\Firefox\\\\Homepage",
+ "valuename": "URL",
+ "class": "USER",
+ "type": 1,
+ "data": "samba.org"
+ },
+ {
+ "keyname": "Software\\\\Microsoft\\\\Internet Explorer\\\\Toolbar",
+ "valuename": "IEToolbar",
+ "class": "USER",
+ "type": "REG_BINARY",
+ "data": [0]
+ },
+ {
+ "keyname": "Software\\\\Policies\\\\Microsoft\\\\InputPersonalization",
+ "valuename": "RestrictImplicitTextCollection",
+ "class": "USER",
+ "type": "REG_DWORD",
+ "data": 1
+ },
+ {
+ "keyname": "Software\\\\Policies\\\\Mozilla\\\\Firefox",
+ "valuename": "ExtensionSettings",
+ "class": "MACHINE",
+ "type": "REG_MULTI_SZ",
+ "data": [
+ "{",
+ " \\"key\\": \\"value\\"",
+ "}"
+ ]
+ }
+]
+"""
+
+gpo_remove_json = \
+b"""
+[
+ {
+ "keyname": "Software\\\\Policies\\\\Mozilla\\\\Firefox\\\\Homepage",
+ "valuename": "StartPage",
+ "class": "USER"
+ },
+ {
+ "keyname": "Software\\\\Policies\\\\Mozilla\\\\Firefox\\\\Homepage",
+ "valuename": "URL",
+ "class": "USER"
+ },
+ {
+ "keyname": "Software\\\\Microsoft\\\\Internet Explorer\\\\Toolbar",
+ "valuename": "IEToolbar",
+ "class": "USER"
+ },
+ {
+ "keyname": "Software\\\\Policies\\\\Microsoft\\\\InputPersonalization",
+ "valuename": "RestrictImplicitTextCollection",
+ "class": "USER"
+ },
+ {
+ "keyname": "Software\\\\Policies\\\\Mozilla\\\\Firefox",
+ "valuename": "ExtensionSettings",
+ "class": "MACHINE"
+ }
+]
+"""
+
+def gpt_ini_version(gpo_guid):
+ lp = LoadParm()
+ lp.load(os.environ['SERVERCONFFILE'])
+ local_path = lp.get('path', 'sysvol')
+ GPT_INI = os.path.join(local_path, lp.get('realm').lower(), 'Policies',
+ gpo_guid, 'GPT.INI')
+ if os.path.exists(GPT_INI):
+ with open(GPT_INI, 'rb') as f:
+ data = f.read()
+ parser = GPTIniParser()
+ parser.parse(data)
+ if parser.ini_conf.has_option('General', 'Version'):
+ version = int(parser.ini_conf.get('General',
+ 'Version').encode('utf-8'))
+ else:
+ version = 0
+ else:
+ version = 0
+ return version
+
+# These are new GUIDs, not used elsewhere, made up for the use of testing the
+# adding of extension GUIDs in `samba-tool gpo load`.
+ext_guids = ['{123d2b56-7b14-4516-bbc4-763d29d57654}',
+ '{d000e91b-e70f-481b-9549-58de7929bcee}']
+
+source_path = os.path.abspath(os.path.join(os.path.dirname(__file__), "../../../../.."))
+provision_path = os.path.join(source_path, "source4/selftest/provisions/")
+
+def has_difference(path1, path2, binary=True, xml=True, sortlines=False):
+ """Use this function to determine if the GPO backup differs from another.
+
+ xml=True checks whether any xml files are equal
+ binary=True checks whether any .SAMBABACKUP files are equal
+ """
+ if os.path.isfile(path1):
+ if sortlines:
+ file1 = open(path1).readlines()
+ file1.sort()
+ file2 = open(path1).readlines()
+ file2.sort()
+ if file1 != file2:
+ return path1
+
+ elif open(path1).read() != open(path2).read():
+ return path1
+
+ return None
+
+ l_dirs = [ path1 ]
+ r_dirs = [ path2 ]
+ while l_dirs:
+ l_dir = l_dirs.pop()
+ r_dir = r_dirs.pop()
+
+ dirlist = os.listdir(l_dir)
+ dirlist_other = os.listdir(r_dir)
+
+ dirlist.sort()
+ dirlist_other.sort()
+ if dirlist != dirlist_other:
+ return dirlist
+
+ for e in dirlist:
+ l_name = os.path.join(l_dir, e)
+ r_name = os.path.join(r_dir, e)
+
+ if os.path.isdir(l_name):
+ l_dirs.append(l_name)
+ r_dirs.append(r_name)
+ else:
+ if (l_name.endswith('.xml') and xml or
+ l_name.endswith('.SAMBABACKUP') and binary):
+ if open(l_name, "rb").read() != open(r_name, "rb").read():
+ return l_name
+
+ return None
+
+
+class GpoCmdTestCase(SambaToolCmdTest):
+ """Tests for samba-tool time subcommands"""
+
+ gpo_name = "testgpo"
+
+ # This exists in the source tree to be restored
+ backup_gpo_guid = "{1E1DC8EA-390C-4800-B327-98B56A0AEA5D}"
+
+ def test_gpo_list(self):
+ """Run gpo list against the server and make sure it looks accurate"""
+ (result, out, err) = self.runsubcmd("gpo", "listall", "-H", "ldap://%s" % os.environ["SERVER"])
+ self.assertCmdSuccess(result, out, err, "Ensuring gpo listall ran successfully")
+
+ def test_fetchfail(self):
+ """Run against a non-existent GPO, and make sure it fails (this hard-coded UUID is very unlikely to exist"""
+ (result, out, err) = self.runsubcmd("gpo", "fetch", "c25cac17-a02a-4151-835d-fae17446ee43", "-H", "ldap://%s" % os.environ["SERVER"])
+ self.assertCmdFail(result, "check for result code")
+
+ def test_fetch(self):
+ """Run against a real GPO, and make sure it passes"""
+ (result, out, err) = self.runsubcmd("gpo", "fetch", self.gpo_guid, "-H", "ldap://%s" % os.environ["SERVER"], "--tmpdir", self.tempdir)
+ self.assertCmdSuccess(result, out, err, "Ensuring gpo fetched successfully")
+ shutil.rmtree(os.path.join(self.tempdir, "policy"))
+
+ def test_show(self):
+ """Show a real GPO, and make sure it passes"""
+ (result, out, err) = self.runsubcmd("gpo", "show", self.gpo_guid, "-H", "ldap://%s" % os.environ["SERVER"])
+ self.assertCmdSuccess(result, out, err, "Ensuring gpo fetched successfully")
+
+ def test_show_as_admin(self):
+ """Show a real GPO, and make sure it passes"""
+ (result, out, err) = self.runsubcmd("gpo", "show", self.gpo_guid, "-H", "ldap://%s" % os.environ["SERVER"], "-U%s%%%s" % (os.environ["USERNAME"], os.environ["PASSWORD"]))
+ self.assertCmdSuccess(result, out, err, "Ensuring gpo fetched successfully")
+
+ def test_aclcheck(self):
+ """Check all the GPOs on the remote server have correct ACLs"""
+ (result, out, err) = self.runsubcmd("gpo", "aclcheck", "-H", "ldap://%s" % os.environ["SERVER"], "-U%s%%%s" % (os.environ["USERNAME"], os.environ["PASSWORD"]))
+ self.assertCmdSuccess(result, out, err, "Ensuring gpo checked successfully")
+
+ def test_getlink_empty(self):
+ self.samdb = self.getSamDB("-H", "ldap://%s" % os.environ["DC_SERVER"],
+ "-U%s%%%s" % (os.environ["DC_USERNAME"],
+ os.environ["DC_PASSWORD"]))
+
+ container_dn = 'OU=gpo_test_link,%s' % self.samdb.get_default_basedn()
+
+ self.samdb.add({
+ 'dn': container_dn,
+ 'objectClass': 'organizationalUnit'
+ })
+
+ (result, out, err) = self.runsubcmd("gpo", "getlink", container_dn,
+ "-H", "ldap://%s" % os.environ["SERVER"],
+ "-U%s%%%s" % (os.environ["USERNAME"],
+ os.environ["PASSWORD"]))
+ self.assertCmdSuccess(result, out, err, "Ensuring gpo link fetched successfully")
+
+ # Microsoft appears to allow an empty space character after deletion of
+ # a GPO. We should be able to handle this.
+ m = ldb.Message()
+ m.dn = ldb.Dn(self.samdb, container_dn)
+ m['gPLink'] = ldb.MessageElement(' ', ldb.FLAG_MOD_REPLACE, 'gPLink')
+ self.samdb.modify(m)
+
+ (result, out, err) = self.runsubcmd("gpo", "getlink", container_dn,
+ "-H", "ldap://%s" % os.environ["SERVER"],
+ "-U%s%%%s" % (os.environ["USERNAME"],
+ os.environ["PASSWORD"]))
+ self.assertCmdSuccess(result, out, err, "Ensuring gpo link fetched successfully")
+
+ self.samdb.delete(container_dn)
+
+ def test_backup_restore_compare_binary(self):
+ """Restore from a static backup and compare the binary contents"""
+
+ if not os.path.exists(provision_path):
+ self.skipTest('Test requires provision data not available in '
+ + 'release tarball')
+
+ static_path = os.path.join(self.backup_path, 'policy',
+ self.backup_gpo_guid)
+
+ temp_path = os.path.join(self.tempdir, 'temp')
+ os.mkdir(temp_path)
+
+ new_path = os.path.join(self.tempdir, 'new')
+ os.mkdir(new_path)
+
+ gpo_guid = None
+ try:
+ (result, out, err) = self.runsubcmd("gpo", "restore", "BACKUP_RESTORE1",
+ static_path,
+ "-H", "ldap://%s" %
+ os.environ["SERVER"], "--tmpdir",
+ temp_path, "--entities",
+ self.entity_file, "-U%s%%%s" %
+ (os.environ["USERNAME"],
+ os.environ["PASSWORD"]),
+ "--restore-metadata")
+
+ self.assertCmdSuccess(result, out, err,
+ "Ensure gpo restore successful")
+
+ gpo_guid = "{%s}" % out.split("{")[1].split("}")[0]
+
+ (result, out, err) = self.runsubcmd("gpo", "backup", gpo_guid,
+ "-H", "ldap://%s" %
+ os.environ["SERVER"],
+ "--tmpdir", new_path)
+
+ self.assertCmdSuccess(result, out, err, "Ensuring gpo fetched successfully")
+
+ # Compare the directories
+ self.assertIsNone(has_difference(os.path.join(new_path, 'policy',
+ gpo_guid),
+ static_path, binary=True,
+ xml=False))
+ finally:
+ if gpo_guid:
+ (result, out, err) = self.runsubcmd("gpo", "del", gpo_guid,
+ "-H", "ldap://%s" %
+ os.environ["SERVER"],
+ "-U%s%%%s" %
+ (os.environ["USERNAME"],
+ os.environ["PASSWORD"]))
+ self.assertCmdSuccess(result, out, err, "Ensuring gpo deleted successfully")
+
+ shutil.rmtree(temp_path)
+ shutil.rmtree(new_path)
+
+ def test_backup_restore_no_entities_compare_binary(self):
+ """Restore from a static backup (and use no entity file, resulting in
+ copy-restore fallback), and compare the binary contents"""
+
+ if not os.path.exists(provision_path):
+ self.skipTest('Test requires provision data not available in '
+ + 'release tarball')
+
+ static_path = os.path.join(self.backup_path, 'policy',
+ self.backup_gpo_guid)
+
+ temp_path = os.path.join(self.tempdir, 'temp')
+ os.mkdir(temp_path)
+
+ new_path = os.path.join(self.tempdir, 'new')
+ os.mkdir(new_path)
+
+ gpo_guid = None
+ gpo_guid1 = None
+ gpo_guid2 = None
+ try:
+ (result, out, err) = self.runsubcmd("gpo", "restore", "BACKUP_RESTORE1",
+ static_path,
+ "-H", "ldap://%s" %
+ os.environ["SERVER"], "--tmpdir",
+ temp_path, "--entities",
+ self.entity_file, "-U%s%%%s" %
+ (os.environ["USERNAME"],
+ os.environ["PASSWORD"]),
+ "--restore-metadata")
+
+ self.assertCmdSuccess(result, out, err,
+ "Ensure gpo restore successful")
+
+ gpo_guid = "{%s}" % out.split("{")[1].split("}")[0]
+ gpo_guid1 = gpo_guid
+
+ # Do not output entities file
+ (result, out, err) = self.runsubcmd("gpo", "backup", gpo_guid,
+ "-H", "ldap://%s" %
+ os.environ["SERVER"],
+ "--tmpdir", new_path,
+ "--generalize")
+
+ self.assertCmdSuccess(result, out, err, "Ensuring gpo fetched successfully")
+
+ # Do not use an entities file
+ (result, out, err) = self.runsubcmd("gpo", "restore", "BACKUP_RESTORE2",
+ os.path.join(new_path, 'policy', gpo_guid1),
+ "-H", "ldap://%s" %
+ os.environ["SERVER"], "--tmpdir",
+ temp_path, "-U%s%%%s" %
+ (os.environ["USERNAME"],
+ os.environ["PASSWORD"]),
+ "--restore-metadata")
+
+ self.assertCmdSuccess(result, out, err,
+ "Ensure gpo restore successful")
+
+ gpo_guid = "{%s}" % out.split("{")[1].split("}")[0]
+ gpo_guid2 = gpo_guid
+
+ self.assertCmdSuccess(result, out, err, "Ensuring gpo restored successfully")
+
+ (result, out, err) = self.runsubcmd("gpo", "backup", gpo_guid,
+ "-H", "ldap://%s" %
+ os.environ["SERVER"],
+ "--tmpdir", new_path)
+
+ # Compare the directories
+ self.assertIsNone(has_difference(os.path.join(new_path, 'policy',
+ gpo_guid1),
+ os.path.join(new_path, 'policy',
+ gpo_guid2),
+ binary=True, xml=False))
+ finally:
+ if gpo_guid1:
+ (result, out, err) = self.runsubcmd("gpo", "del", gpo_guid1,
+ "-H", "ldap://%s" %
+ os.environ["SERVER"],
+ "-U%s%%%s" %
+ (os.environ["USERNAME"],
+ os.environ["PASSWORD"]))
+ self.assertCmdSuccess(result, out, err, "Ensuring gpo deleted successfully")
+
+ if gpo_guid2:
+ (result, out, err) = self.runsubcmd("gpo", "del", gpo_guid2,
+ "-H", "ldap://%s" %
+ os.environ["SERVER"],
+ "-U%s%%%s" %
+ (os.environ["USERNAME"],
+ os.environ["PASSWORD"]))
+ self.assertCmdSuccess(result, out, err, "Ensuring gpo deleted successfully")
+
+ shutil.rmtree(temp_path)
+ shutil.rmtree(new_path)
+
+ def test_backup_restore_backup_compare_XML(self):
+ """Restore from a static backup and backup to compare XML"""
+
+ if not os.path.exists(provision_path):
+ self.skipTest('Test requires provision data not available in '
+ + 'release tarball')
+
+ static_path = os.path.join(self.backup_path, 'policy',
+ self.backup_gpo_guid)
+
+ temp_path = os.path.join(self.tempdir, 'temp')
+ os.mkdir(temp_path)
+
+ new_path = os.path.join(self.tempdir, 'new')
+ os.mkdir(new_path)
+
+ gpo_guid = None
+ gpo_guid1 = None
+ gpo_guid2 = None
+ try:
+ (result, out, err) = self.runsubcmd("gpo", "restore", "BACKUP_RESTORE1",
+ static_path,
+ "-H", "ldap://%s" %
+ os.environ["SERVER"], "--tmpdir",
+ temp_path, "--entities",
+ self.entity_file, "-U%s%%%s" %
+ (os.environ["USERNAME"],
+ os.environ["PASSWORD"]),
+ "--restore-metadata")
+
+ self.assertCmdSuccess(result, out, err,
+ "Ensure gpo restore successful")
+
+ gpo_guid = "{%s}" % out.split("{")[1].split("}")[0]
+ gpo_guid1 = gpo_guid
+
+ (result, out, err) = self.runsubcmd("gpo", "backup", gpo_guid,
+ "-H", "ldap://%s" %
+ os.environ["SERVER"],
+ "--tmpdir", new_path)
+
+ self.assertCmdSuccess(result, out, err, "Ensuring gpo fetched successfully")
+
+ (result, out, err) = self.runsubcmd("gpo", "restore", "BACKUP_RESTORE2",
+ os.path.join(new_path, 'policy', gpo_guid1),
+ "-H", "ldap://%s" %
+ os.environ["SERVER"], "--tmpdir",
+ temp_path, "--entities",
+ self.entity_file, "-U%s%%%s" %
+ (os.environ["USERNAME"],
+ os.environ["PASSWORD"]),
+ "--restore-metadata")
+
+ self.assertCmdSuccess(result, out, err,
+ "Ensure gpo restore successful")
+
+ gpo_guid = "{%s}" % out.split("{")[1].split("}")[0]
+ gpo_guid2 = gpo_guid
+
+ self.assertCmdSuccess(result, out, err, "Ensuring gpo restored successfully")
+
+ (result, out, err) = self.runsubcmd("gpo", "backup", gpo_guid,
+ "-H", "ldap://%s" %
+ os.environ["SERVER"],
+ "--tmpdir", new_path)
+
+ # Compare the directories
+ self.assertIsNone(has_difference(os.path.join(new_path, 'policy',
+ gpo_guid1),
+ os.path.join(new_path, 'policy',
+ gpo_guid2),
+ binary=True, xml=True))
+ finally:
+ if gpo_guid1:
+ (result, out, err) = self.runsubcmd("gpo", "del", gpo_guid1,
+ "-H", "ldap://%s" %
+ os.environ["SERVER"],
+ "-U%s%%%s" %
+ (os.environ["USERNAME"],
+ os.environ["PASSWORD"]))
+ self.assertCmdSuccess(result, out, err, "Ensuring gpo deleted successfully")
+
+ if gpo_guid2:
+ (result, out, err) = self.runsubcmd("gpo", "del", gpo_guid2,
+ "-H", "ldap://%s" %
+ os.environ["SERVER"],
+ "-U%s%%%s" %
+ (os.environ["USERNAME"],
+ os.environ["PASSWORD"]))
+ self.assertCmdSuccess(result, out, err, "Ensuring gpo deleted successfully")
+
+ shutil.rmtree(temp_path)
+ shutil.rmtree(new_path)
+
+ def test_backup_restore_generalize(self):
+ """Restore from a static backup with different entities, generalize it
+ again, and compare the XML"""
+
+ if not os.path.exists(provision_path):
+ self.skipTest('Test requires provision data not available in '
+ + 'release tarball')
+
+ static_path = os.path.join(self.backup_path, 'policy',
+ self.backup_gpo_guid)
+
+ temp_path = os.path.join(self.tempdir, 'temp')
+ os.mkdir(temp_path)
+
+ new_path = os.path.join(self.tempdir, 'new')
+ os.mkdir(new_path)
+
+ alt_entity_file = os.path.join(new_path, 'entities')
+ with open(alt_entity_file, 'wb') as f:
+ f.write(b'''<!ENTITY SAMBA__NETWORK_PATH__82419dafed126a07d6b96c66fc943735__ "\\\\samdom.example.com">
+<!ENTITY SAMBA__NETWORK_PATH__0484cd41ded45a0728333a9c5e5ef619__ "\\\\samdom">
+<!ENTITY SAMBA____SDDL_ACL____4ce8277be3f630300cbcf80a80e21cf4__ "D:PAR(A;CI;KA;;;BA)(A;CIIO;KA;;;CO)(A;CI;KA;;;SY)(A;CI;KR;;;S-1-16-0)">
+<!ENTITY SAMBA____USER_ID_____d0970f5a1e19cb803f916c203d5c39c4__ "*S-1-5-113">
+<!ENTITY SAMBA____USER_ID_____7b7bc2512ee1fedcd76bdc68926d4f7b__ "Administrator">
+<!ENTITY SAMBA____USER_ID_____a3069f5a7a6530293ad8df6abd32af3d__ "Foobaz">
+<!ENTITY SAMBA____USER_ID_____fdf60b2473b319c8c341de5f62479a7d__ "*S-1-5-32-545">
+<!ENTITY SAMBA____USER_ID_____adb831a7fdd83dd1e2a309ce7591dff8__ "Guest">
+<!ENTITY SAMBA____USER_ID_____9fa835214b4fc8b6102c991f7d97c2f8__ "*S-1-5-32-547">
+<!ENTITY SAMBA____USER_ID_____bf8caafa94a19a6262bad2e8b6d4bce6__ "*S-1-5-32-546">
+<!ENTITY SAMBA____USER_ID_____a45da96d0bf6575970f2d27af22be28a__ "System">
+<!ENTITY SAMBA____USER_ID_____171d33a63ebd67f856552940ed491ad3__ "s-1-5-32-545">
+<!ENTITY SAMBA____USER_ID_____7140932fff16ce85cc64d3caab588d0d__ "s-1-1-0">
+''')
+
+ gen_entity_file = os.path.join(temp_path, 'entities')
+
+ gpo_guid = None
+ try:
+ (result, out, err) = self.runsubcmd("gpo", "restore", "BACKUP_RESTORE1",
+ static_path,
+ "-H", "ldap://%s" %
+ os.environ["SERVER"], "--tmpdir",
+ temp_path, "--entities",
+ alt_entity_file, "-U%s%%%s" %
+ (os.environ["USERNAME"],
+ os.environ["PASSWORD"]),
+ "--restore-metadata")
+
+ self.assertCmdSuccess(result, out, err, "Ensuring gpo restored successfully")
+
+ gpo_guid = "{%s}" % out.split("{")[1].split("}")[0]
+
+ (result, out, err) = self.runsubcmd("gpo", "backup", gpo_guid,
+ "-H", "ldap://%s" %
+ os.environ["SERVER"],
+ "--tmpdir", new_path,
+ "--generalize", "--entities",
+ gen_entity_file)
+
+ self.assertCmdSuccess(result, out, err, "Ensuring gpo fetched successfully")
+
+ # Assert entity files are identical (except for line order)
+ self.assertIsNone(has_difference(alt_entity_file,
+ gen_entity_file,
+ sortlines=True))
+
+ # Compare the directories (XML)
+ self.assertIsNone(has_difference(os.path.join(new_path, 'policy',
+ gpo_guid),
+ static_path, binary=False,
+ xml=True))
+ finally:
+ if gpo_guid:
+ (result, out, err) = self.runsubcmd("gpo", "del", gpo_guid,
+ "-H", "ldap://%s" %
+ os.environ["SERVER"],
+ "-U%s%%%s" %
+ (os.environ["USERNAME"],
+ os.environ["PASSWORD"]))
+ self.assertCmdSuccess(result, out, err, "Ensuring gpo deleted successfully")
+
+ shutil.rmtree(temp_path)
+ shutil.rmtree(new_path)
+
+ def test_backup_with_extension_attributes(self):
+ self.samdb = self.getSamDB("-H", "ldap://%s" % os.environ["DC_SERVER"],
+ "-U%s%%%s" % (os.environ["DC_USERNAME"],
+ os.environ["DC_PASSWORD"]))
+
+ temp_path = os.path.join(self.tempdir, 'temp')
+ os.mkdir(temp_path)
+
+ extensions = {
+ # Taken from "source4/setup/provision_group_policy.ldif" on domain
+ 'gPCMachineExtensionNames': '[{35378EAC-683F-11D2-A89A-00C04FBBCFA2}{53D6AB1B-2488-11D1-A28C-00C04FB94F17}][{827D319E-6EAC-11D2-A4EA-00C04F79F83A}{803E14A0-B4FB-11D0-A0D0-00A0C90F574B}][{B1BE8D72-6EAC-11D2-A4EA-00C04F79F83A}{53D6AB1B-2488-11D1-A28C-00C04FB94F17}]',
+ 'gPCUserExtensionNames': '[{3060E8D0-7020-11D2-842D-00C04FA372D4}{3060E8CE-7020-11D2-842D-00C04FA372D4}][{35378EAC-683F-11D2-A89A-00C04FBBCFA2}{0F6B957E-509E-11D1-A7CC-0000F87571E3}]'
+ }
+
+ gpo_dn = get_gpo_dn(self.samdb, self.gpo_guid)
+ for ext in extensions:
+ data = extensions[ext]
+
+ m = ldb.Message()
+ m.dn = gpo_dn
+ m[ext] = ldb.MessageElement(data, ldb.FLAG_MOD_REPLACE, ext)
+
+ self.samdb.modify(m)
+
+ try:
+ (result, out, err) = self.runsubcmd("gpo", "backup", self.gpo_guid,
+ "-H", "ldap://%s" %
+ os.environ["SERVER"],
+ "--tmpdir", temp_path)
+
+ self.assertCmdSuccess(result, out, err, "Ensuring gpo fetched successfully")
+
+ guid = "{%s}" % out.split("{")[1].split("}")[0]
+
+ temp_path = os.path.join(temp_path, 'policy', guid)
+
+ (result, out, err) = self.runsubcmd("gpo", "restore", "RESTORE_EXT",
+ temp_path,
+ "-H", "ldap://%s" %
+ os.environ["SERVER"], "--tmpdir",
+ self.tempdir, "-U%s%%%s" %
+ (os.environ["USERNAME"],
+ os.environ["PASSWORD"]),
+ "--restore-metadata")
+
+ self.assertCmdSuccess(result, out, err, "Ensuring gpo restored successfully")
+
+ gpo_guid = "{%s}" % out.split("{")[1].split("}")[0]
+
+ msg = get_gpo_info(self.samdb, gpo_guid)
+ self.assertEqual(len(msg), 1)
+
+ for ext in extensions:
+ self.assertTrue(ext in msg[0])
+ self.assertEqual(extensions[ext], str(msg[0][ext][0]))
+
+ finally:
+ if gpo_guid:
+ (result, out, err) = self.runsubcmd("gpo", "del", gpo_guid,
+ "-H", "ldap://%s" %
+ os.environ["SERVER"],
+ "-U%s%%%s" %
+ (os.environ["USERNAME"],
+ os.environ["PASSWORD"]))
+ self.assertCmdSuccess(result, out, err, "Ensuring gpo deleted successfully")
+
+ shutil.rmtree(os.path.join(self.tempdir, "policy"))
+ shutil.rmtree(os.path.join(self.tempdir, 'temp'))
+
+ def test_admx_load(self):
+ lp = LoadParm()
+ lp.load(os.environ['SERVERCONFFILE'])
+ local_path = lp.get('path', 'sysvol')
+ admx_path = os.path.join(local_path, os.environ['REALM'].lower(),
+ 'Policies', 'PolicyDefinitions')
+ (result, out, err) = self.runsubcmd("gpo", "admxload",
+ "-H", "ldap://%s" %
+ os.environ["SERVER"],
+ "--admx-dir=%s" %
+ os.path.join(source_path,
+ 'libgpo/admx'),
+ "-U%s%%%s" %
+ (os.environ["USERNAME"],
+ os.environ["PASSWORD"]))
+ self.assertCmdSuccess(result, out, err,
+ 'Filling PolicyDefinitions failed')
+ self.assertTrue(os.path.exists(admx_path),
+ 'PolicyDefinitions was not created')
+ self.assertTrue(os.path.exists(os.path.join(admx_path, 'samba.admx')),
+ 'Filling PolicyDefinitions failed')
+ shutil.rmtree(admx_path)
+
+ def test_smb_conf_set(self):
+ lp = LoadParm()
+ lp.load(os.environ['SERVERCONFFILE'])
+ local_path = lp.get('path', 'sysvol')
+ reg_pol = os.path.join(local_path, lp.get('realm').lower(), 'Policies',
+ self.gpo_guid, 'Machine/Registry.pol')
+
+ policy = 'apply group policies'
+ before_vers = gpt_ini_version(self.gpo_guid)
+ (result, out, err) = self.runsublevelcmd("gpo", ("manage", "smb_conf",
+ "set"), self.gpo_guid,
+ policy, "yes",
+ "-H", "ldap://%s" %
+ os.environ["SERVER"],
+ "-U%s%%%s" %
+ (os.environ["USERNAME"],
+ os.environ["PASSWORD"]))
+ self.assertCmdSuccess(result, out, err,
+ 'Failed to set apply group policies')
+ after_vers = gpt_ini_version(self.gpo_guid)
+ self.assertGreater(after_vers, before_vers, 'GPT.INI was not updated')
+
+ self.assertTrue(os.path.exists(reg_pol),
+ 'The Registry.pol does not exist')
+ reg_data = ndr_unpack(preg.file, open(reg_pol, 'rb').read())
+ ret = any([get_string(e.valuename) == policy and e.data == 1
+ for e in reg_data.entries])
+ self.assertTrue(ret, 'The sudoers entry was not added')
+
+ before_vers = after_vers
+ # Ensure an empty set command deletes the entry
+ (result, out, err) = self.runsublevelcmd("gpo", ("manage", "smb_conf",
+ "set"), self.gpo_guid,
+ policy, "-H", "ldap://%s" %
+ os.environ["SERVER"],
+ "-U%s%%%s" %
+ (os.environ["USERNAME"],
+ os.environ["PASSWORD"]))
+ self.assertCmdSuccess(result, out, err,
+ 'Failed to unset apply group policies')
+ after_vers = gpt_ini_version(self.gpo_guid)
+ self.assertGreater(after_vers, before_vers, 'GPT.INI was not updated')
+
+ reg_data = ndr_unpack(preg.file, open(reg_pol, 'rb').read())
+ ret = not any([get_string(e.valuename) == policy and e.data == 1
+ for e in reg_data.entries])
+ self.assertTrue(ret, 'The sudoers entry was not removed')
+
+ def test_smb_conf_list(self):
+ lp = LoadParm()
+ lp.load(os.environ['SERVERCONFFILE'])
+ local_path = lp.get('path', 'sysvol')
+ reg_pol = os.path.join(local_path, lp.get('realm').lower(), 'Policies',
+ self.gpo_guid, 'Machine/Registry.pol')
+
+ # Stage the Registry.pol file with test data
+ stage = preg.file()
+ e = preg.entry()
+ e.keyname = b'Software\\Policies\\Samba\\smb_conf'
+ e.valuename = b'apply group policies'
+ e.type = 4
+ e.data = 1
+ stage.num_entries = 1
+ stage.entries = [e]
+ ret = stage_file(reg_pol, ndr_pack(stage))
+ self.assertTrue(ret, 'Could not create the target %s' % reg_pol)
+
+ (result, out, err) = self.runsublevelcmd("gpo", ("manage", "smb_conf",
+ "list"), self.gpo_guid,
+ "-H", "ldap://%s" %
+ os.environ["SERVER"],
+ "-U%s%%%s" %
+ (os.environ["USERNAME"],
+ os.environ["PASSWORD"]))
+ self.assertIn('%s = True' % e.valuename, out, 'The test entry was not found!')
+
+ # Unstage the Registry.pol file
+ unstage_file(reg_pol)
+
+ def test_security_set(self):
+ lp = LoadParm()
+ lp.load(os.environ['SERVERCONFFILE'])
+ local_path = lp.get('path', 'sysvol')
+ inf_pol = os.path.join(local_path, lp.get('realm').lower(), 'Policies',
+ self.gpo_guid, 'Machine/Microsoft/Windows NT/SecEdit/GptTmpl.inf')
+
+ before_vers = gpt_ini_version(self.gpo_guid)
+ (result, out, err) = self.runsublevelcmd("gpo", ("manage", "security",
+ "set"), self.gpo_guid,
+ 'MaxTicketAge', '10',
+ "-H", "ldap://%s" %
+ os.environ["SERVER"],
+ "-U%s%%%s" %
+ (os.environ["USERNAME"],
+ os.environ["PASSWORD"]))
+ self.assertCmdSuccess(result, out, err,
+ 'Failed to set MaxTicketAge')
+ self.assertTrue(os.path.exists(inf_pol),
+ '%s was not created' % inf_pol)
+ inf_pol_contents = open(inf_pol, 'r').read()
+ self.assertIn('MaxTicketAge = 10', inf_pol_contents,
+ 'The test entry was not found!')
+ after_vers = gpt_ini_version(self.gpo_guid)
+ self.assertGreater(after_vers, before_vers, 'GPT.INI was not updated')
+
+ before_vers = after_vers
+ # Ensure an empty set command deletes the entry
+ (result, out, err) = self.runsublevelcmd("gpo", ("manage", "security",
+ "set"), self.gpo_guid,
+ 'MaxTicketAge',
+ "-H", "ldap://%s" %
+ os.environ["SERVER"],
+ "-U%s%%%s" %
+ (os.environ["USERNAME"],
+ os.environ["PASSWORD"]))
+ self.assertCmdSuccess(result, out, err,
+ 'Failed to unset MaxTicketAge')
+ inf_pol_contents = open(inf_pol, 'r').read()
+ self.assertNotIn('MaxTicketAge = 10', inf_pol_contents,
+ 'The test entry was still found!')
+ after_vers = gpt_ini_version(self.gpo_guid)
+ self.assertGreater(after_vers, before_vers, 'GPT.INI was not updated')
+
+ def test_security_list(self):
+ (result, out, err) = self.runsublevelcmd("gpo", ("manage", "security",
+ "set"), self.gpo_guid,
+ 'MaxTicketAge', '10',
+ "-H", "ldap://%s" %
+ os.environ["SERVER"],
+ "-U%s%%%s" %
+ (os.environ["USERNAME"],
+ os.environ["PASSWORD"]))
+ self.assertCmdSuccess(result, out, err,
+ 'Failed to set MaxTicketAge')
+
+ (result, out, err) = self.runsublevelcmd("gpo", ("manage", "security",
+ "list"), self.gpo_guid,
+ "-H", "ldap://%s" %
+ os.environ["SERVER"],
+ "-U%s%%%s" %
+ (os.environ["USERNAME"],
+ os.environ["PASSWORD"]))
+ self.assertIn('MaxTicketAge = 10', out, 'The test entry was not found!')
+
+ (result, out, err) = self.runsublevelcmd("gpo", ("manage", "security",
+ "set"), self.gpo_guid,
+ 'MaxTicketAge',
+ "-H", "ldap://%s" %
+ os.environ["SERVER"],
+ "-U%s%%%s" %
+ (os.environ["USERNAME"],
+ os.environ["PASSWORD"]))
+ self.assertCmdSuccess(result, out, err,
+ 'Failed to unset MaxTicketAge')
+
+ def test_security_nonempty_sections(self):
+ lp = LoadParm()
+ lp.load(os.environ['SERVERCONFFILE'])
+ local_path = lp.get('path', 'sysvol')
+ gpt_inf = os.path.join(local_path, lp.get('realm').lower(), 'Policies',
+ self.gpo_guid, 'Machine/Microsoft/Windows NT',
+ 'SecEdit/GptTmpl.inf')
+
+ before_vers = gpt_ini_version(self.gpo_guid)
+ (result, out, err) = self.runsublevelcmd("gpo", ("manage", "security",
+ "set"), self.gpo_guid,
+ 'MaxTicketAge', '10',
+ "-H", "ldap://%s" %
+ os.environ["SERVER"],
+ "-U%s%%%s" %
+ (os.environ["USERNAME"],
+ os.environ["PASSWORD"]))
+ self.assertCmdSuccess(result, out, err,
+ 'Failed to set MaxTicketAge')
+ after_vers = gpt_ini_version(self.gpo_guid)
+ self.assertGreater(after_vers, before_vers, 'GPT.INI was not updated')
+
+ before_vers = after_vers
+ (result, out, err) = self.runsublevelcmd("gpo", ("manage", "security",
+ "set"), self.gpo_guid,
+ 'MaxTicketAge',
+ "-H", "ldap://%s" %
+ os.environ["SERVER"],
+ "-U%s%%%s" %
+ (os.environ["USERNAME"],
+ os.environ["PASSWORD"]))
+ self.assertCmdSuccess(result, out, err,
+ 'Failed to unset MaxTicketAge')
+ after_vers = gpt_ini_version(self.gpo_guid)
+ self.assertGreater(after_vers, before_vers, 'GPT.INI was not updated')
+
+ inf_data = ConfigParser(interpolation=None)
+ inf_data.read(gpt_inf)
+
+ self.assertFalse(inf_data.has_section('Kerberos Policy'))
+
+ def test_sudoers_add(self):
+ lp = LoadParm()
+ lp.load(os.environ['SERVERCONFFILE'])
+ local_path = lp.get('path', 'sysvol')
+ reg_pol = os.path.join(local_path, lp.get('realm').lower(), 'Policies',
+ self.gpo_guid, 'Machine/Registry.pol')
+
+ # Stage the Registry.pol file with test data
+ stage = preg.file()
+ e = preg.entry()
+ e.keyname = b'Software\\Policies\\Samba\\Unix Settings\\Sudo Rights'
+ e.valuename = b'Software\\Policies\\Samba\\Unix Settings'
+ e.type = 1
+ e.data = b'fakeu ALL=(ALL) NOPASSWD: ALL'
+ stage.num_entries = 1
+ stage.entries = [e]
+ ret = stage_file(reg_pol, ndr_pack(stage))
+ self.assertTrue(ret, 'Could not create the target %s' % reg_pol)
+
+ before_vers = gpt_ini_version(self.gpo_guid)
+ (result, out, err) = self.runsublevelcmd("gpo", ("manage",
+ "sudoers", "add"),
+ self.gpo_guid, 'ALL', 'ALL',
+ 'fakeu', 'fakeg', "-H",
+ "ldap://%s" %
+ os.environ["SERVER"],
+ "-U%s%%%s" %
+ (os.environ["USERNAME"],
+ os.environ["PASSWORD"]))
+ self.assertCmdSuccess(result, out, err, 'Sudoers add failed')
+ after_vers = gpt_ini_version(self.gpo_guid)
+ self.assertGreater(after_vers, before_vers, 'GPT.INI was not updated')
+
+ sudoer = 'fakeu,fakeg% ALL=(ALL) NOPASSWD: ALL'
+ (result, out, err) = self.runsublevelcmd("gpo", ("manage",
+ "sudoers", "list"),
+ self.gpo_guid, "-H",
+ "ldap://%s" %
+ os.environ["SERVER"],
+ "-U%s%%%s" %
+ (os.environ["USERNAME"],
+ os.environ["PASSWORD"]))
+ self.assertIn(sudoer, out, 'The test entry was not found!')
+ self.assertIn(get_string(e.data), out, 'The test entry was not found!')
+
+ before_vers = after_vers
+ (result, out, err) = self.runsublevelcmd("gpo", ("manage",
+ "sudoers", "remove"),
+ self.gpo_guid, sudoer,
+ "-H", "ldap://%s" %
+ os.environ["SERVER"],
+ "-U%s%%%s" %
+ (os.environ["USERNAME"],
+ os.environ["PASSWORD"]))
+ self.assertCmdSuccess(result, out, err, 'Sudoers remove failed')
+ after_vers = gpt_ini_version(self.gpo_guid)
+ self.assertGreater(after_vers, before_vers, 'GPT.INI was not updated')
+
+ before_vers = after_vers
+ (result, out, err) = self.runsublevelcmd("gpo", ("manage",
+ "sudoers", "remove"),
+ self.gpo_guid,
+ get_string(e.data),
+ "-H", "ldap://%s" %
+ os.environ["SERVER"],
+ "-U%s%%%s" %
+ (os.environ["USERNAME"],
+ os.environ["PASSWORD"]))
+ self.assertCmdSuccess(result, out, err, 'Sudoers remove failed')
+ after_vers = gpt_ini_version(self.gpo_guid)
+ self.assertGreater(after_vers, before_vers, 'GPT.INI was not updated')
+
+ (result, out, err) = self.runsublevelcmd("gpo", ("manage",
+ "sudoers", "list"),
+ self.gpo_guid, "-H",
+ "ldap://%s" %
+ os.environ["SERVER"],
+ "-U%s%%%s" %
+ (os.environ["USERNAME"],
+ os.environ["PASSWORD"]))
+ self.assertNotIn(sudoer, out, 'The test entry was still found!')
+ self.assertNotIn(get_string(e.data), out,
+ 'The test entry was still found!')
+
+ # Unstage the Registry.pol file
+ unstage_file(reg_pol)
+
+ def test_sudoers_list(self):
+ lp = LoadParm()
+ lp.load(os.environ['SERVERCONFFILE'])
+ local_path = lp.get('path', 'sysvol')
+ vgp_xml = os.path.join(local_path, lp.get('realm').lower(), 'Policies',
+ self.gpo_guid, 'Machine/VGP/VTLA/Sudo',
+ 'SudoersConfiguration/manifest.xml')
+
+ stage = etree.Element('vgppolicy')
+ policysetting = etree.SubElement(stage, 'policysetting')
+ pv = etree.SubElement(policysetting, 'version')
+ pv.text = '1'
+ name = etree.SubElement(policysetting, 'name')
+ name.text = 'Sudo Policy'
+ description = etree.SubElement(policysetting, 'description')
+ description.text = 'Sudoers File Configuration Policy'
+ apply_mode = etree.SubElement(policysetting, 'apply_mode')
+ apply_mode.text = 'merge'
+ data = etree.SubElement(policysetting, 'data')
+ load_plugin = etree.SubElement(data, 'load_plugin')
+ load_plugin.text = 'true'
+ sudoers_entry = etree.SubElement(data, 'sudoers_entry')
+ command = etree.SubElement(sudoers_entry, 'command')
+ command.text = 'ALL'
+ user = etree.SubElement(sudoers_entry, 'user')
+ user.text = 'ALL'
+ listelement = etree.SubElement(sudoers_entry, 'listelement')
+ principal = etree.SubElement(listelement, 'principal')
+ principal.text = 'fakeu'
+ principal.attrib['type'] = 'user'
+ # Ensure an empty principal doesn't cause a crash
+ sudoers_entry = etree.SubElement(data, 'sudoers_entry')
+ command = etree.SubElement(sudoers_entry, 'command')
+ command.text = 'ALL'
+ user = etree.SubElement(sudoers_entry, 'user')
+ user.text = 'ALL'
+ # Ensure having dispersed principals still works
+ sudoers_entry = etree.SubElement(data, 'sudoers_entry')
+ command = etree.SubElement(sudoers_entry, 'command')
+ command.text = 'ALL'
+ user = etree.SubElement(sudoers_entry, 'user')
+ user.text = 'ALL'
+ listelement = etree.SubElement(sudoers_entry, 'listelement')
+ principal = etree.SubElement(listelement, 'principal')
+ principal.text = 'fakeu2'
+ principal.attrib['type'] = 'user'
+ listelement = etree.SubElement(sudoers_entry, 'listelement')
+ group = etree.SubElement(listelement, 'principal')
+ group.text = 'fakeg2'
+ group.attrib['type'] = 'group'
+ ret = stage_file(vgp_xml, etree.tostring(stage, 'utf-8'))
+ self.assertTrue(ret, 'Could not create the target %s' % vgp_xml)
+
+ reg_pol = os.path.join(local_path, lp.get('realm').lower(), 'Policies',
+ self.gpo_guid, 'Machine/Registry.pol')
+
+ # Stage the Registry.pol file with test data
+ stage = preg.file()
+ e = preg.entry()
+ e.keyname = b'Software\\Policies\\Samba\\Unix Settings\\Sudo Rights'
+ e.valuename = b'Software\\Policies\\Samba\\Unix Settings'
+ e.type = 1
+ e.data = b'fakeu3 ALL=(ALL) NOPASSWD: ALL'
+ stage.num_entries = 1
+ stage.entries = [e]
+ ret = stage_file(reg_pol, ndr_pack(stage))
+ self.assertTrue(ret, 'Could not create the target %s' % reg_pol)
+
+ sudoer = 'fakeu ALL=(ALL) NOPASSWD: ALL'
+ sudoer2 = 'fakeu2,fakeg2% ALL=(ALL) NOPASSWD: ALL'
+ sudoer_no_principal = 'ALL ALL=(ALL) NOPASSWD: ALL'
+ (result, out, err) = self.runsublevelcmd("gpo", ("manage",
+ "sudoers", "list"),
+ self.gpo_guid, "-H",
+ "ldap://%s" %
+ os.environ["SERVER"],
+ "-U%s%%%s" %
+ (os.environ["USERNAME"],
+ os.environ["PASSWORD"]))
+ self.assertCmdSuccess(result, out, err, 'Sudoers list failed')
+ self.assertIn(sudoer, out, 'The test entry was not found!')
+ self.assertIn(sudoer2, out, 'The test entry was not found!')
+ self.assertIn(get_string(e.data), out, 'The test entry was not found!')
+ self.assertIn(sudoer_no_principal, out,
+ 'The test entry was not found!')
+
+ (result, out, err) = self.runsublevelcmd("gpo", ("manage",
+ "sudoers", "remove"),
+ self.gpo_guid, sudoer2,
+ "-H", "ldap://%s" %
+ os.environ["SERVER"],
+ "-U%s%%%s" %
+ (os.environ["USERNAME"],
+ os.environ["PASSWORD"]))
+ self.assertCmdSuccess(result, out, err, 'Sudoers remove failed')
+
+ (result, out, err) = self.runsublevelcmd("gpo", ("manage",
+ "sudoers", "remove"),
+ self.gpo_guid,
+ sudoer_no_principal,
+ "-H", "ldap://%s" %
+ os.environ["SERVER"],
+ "-U%s%%%s" %
+ (os.environ["USERNAME"],
+ os.environ["PASSWORD"]))
+ self.assertCmdSuccess(result, out, err, 'Sudoers remove failed')
+
+ (result, out, err) = self.runsublevelcmd("gpo", ("manage",
+ "sudoers", "list"),
+ self.gpo_guid, "-H",
+ "ldap://%s" %
+ os.environ["SERVER"],
+ "-U%s%%%s" %
+ (os.environ["USERNAME"],
+ os.environ["PASSWORD"]))
+ self.assertNotIn(sudoer2, out, 'The test entry was still found!')
+ self.assertNotIn(sudoer_no_principal, out,
+ 'The test entry was still found!')
+
+ # Unstage the manifest.xml file
+ unstage_file(vgp_xml)
+ # Unstage the Registry.pol file
+ unstage_file(reg_pol)
+
+ def test_symlink_list(self):
+ lp = LoadParm()
+ lp.load(os.environ['SERVERCONFFILE'])
+ local_path = lp.get('path', 'sysvol')
+ vgp_xml = os.path.join(local_path, lp.get('realm').lower(), 'Policies',
+ self.gpo_guid, 'Machine/VGP/VTLA/Unix',
+ 'Symlink/manifest.xml')
+ stage = etree.Element('vgppolicy')
+ policysetting = etree.SubElement(stage, 'policysetting')
+ pv = etree.SubElement(policysetting, 'version')
+ pv.text = '1'
+ name = etree.SubElement(policysetting, 'name')
+ name.text = 'Symlink Policy'
+ description = etree.SubElement(policysetting, 'description')
+ description.text = 'Specifies symbolic link data'
+ apply_mode = etree.SubElement(policysetting, 'apply_mode')
+ apply_mode.text = 'merge'
+ data = etree.SubElement(policysetting, 'data')
+ file_properties = etree.SubElement(data, 'file_properties')
+ source = etree.SubElement(file_properties, 'source')
+ source.text = os.path.join(self.tempdir, 'test.source')
+ target = etree.SubElement(file_properties, 'target')
+ target.text = os.path.join(self.tempdir, 'test.target')
+ ret = stage_file(vgp_xml, etree.tostring(stage, 'utf-8'))
+ self.assertTrue(ret, 'Could not create the target %s' % vgp_xml)
+
+ symlink = 'ln -s %s %s' % (source.text, target.text)
+ (result, out, err) = self.runsublevelcmd("gpo", ("manage",
+ "symlink", "list"),
+ self.gpo_guid, "-H",
+ "ldap://%s" %
+ os.environ["SERVER"],
+ "-U%s%%%s" %
+ (os.environ["USERNAME"],
+ os.environ["PASSWORD"]))
+ self.assertIn(symlink, out, 'The test entry was not found!')
+
+ # Unstage the manifest.xml file
+ unstage_file(vgp_xml)
+
+ def test_symlink_add(self):
+ source_text = os.path.join(self.tempdir, 'test.source')
+ target_text = os.path.join(self.tempdir, 'test.target')
+ symlink = 'ln -s %s %s' % (source_text, target_text)
+ before_vers = gpt_ini_version(self.gpo_guid)
+ (result, out, err) = self.runsublevelcmd("gpo", ("manage",
+ "symlink", "add"),
+ self.gpo_guid,
+ source_text, target_text,
+ "-H", "ldap://%s" %
+ os.environ["SERVER"],
+ "-U%s%%%s" %
+ (os.environ["USERNAME"],
+ os.environ["PASSWORD"]))
+ self.assertCmdSuccess(result, out, err, 'Symlink add failed')
+ after_vers = gpt_ini_version(self.gpo_guid)
+ self.assertGreater(after_vers, before_vers, 'GPT.INI was not updated')
+
+ (result, out, err) = self.runsublevelcmd("gpo", ("manage",
+ "symlink", "list"),
+ self.gpo_guid, "-H",
+ "ldap://%s" %
+ os.environ["SERVER"],
+ "-U%s%%%s" %
+ (os.environ["USERNAME"],
+ os.environ["PASSWORD"]))
+ self.assertIn(symlink, out, 'The test entry was not found!')
+
+ before_vers = after_vers
+ (result, out, err) = self.runsublevelcmd("gpo", ("manage",
+ "symlink", "remove"),
+ self.gpo_guid,
+ source_text, target_text,
+ "-H", "ldap://%s" %
+ os.environ["SERVER"],
+ "-U%s%%%s" %
+ (os.environ["USERNAME"],
+ os.environ["PASSWORD"]))
+ self.assertCmdSuccess(result, out, err, 'Symlink remove failed')
+ after_vers = gpt_ini_version(self.gpo_guid)
+ self.assertGreater(after_vers, before_vers, 'GPT.INI was not updated')
+
+ (result, out, err) = self.runsublevelcmd("gpo", ("manage",
+ "symlink", "list"),
+ self.gpo_guid, "-H",
+ "ldap://%s" %
+ os.environ["SERVER"],
+ "-U%s%%%s" %
+ (os.environ["USERNAME"],
+ os.environ["PASSWORD"]))
+ self.assertNotIn(symlink, out, 'The test entry was not removed!')
+
+ def test_files_list(self):
+ lp = LoadParm()
+ lp.load(os.environ['SERVERCONFFILE'])
+ local_path = lp.get('path', 'sysvol')
+ vgp_xml = os.path.join(local_path, lp.get('realm').lower(), 'Policies',
+ self.gpo_guid, 'Machine/VGP/VTLA/Unix',
+ 'Files/manifest.xml')
+ source_file = os.path.join(local_path, lp.get('realm').lower(),
+ 'Policies', self.gpo_guid, 'Machine/VGP',
+ 'VTLA/Unix/Files/test.source')
+ stage = etree.Element('vgppolicy')
+ policysetting = etree.SubElement(stage, 'policysetting')
+ pv = etree.SubElement(policysetting, 'version')
+ pv.text = '1'
+ name = etree.SubElement(policysetting, 'name')
+ name.text = 'Files'
+ description = etree.SubElement(policysetting, 'description')
+ description.text = 'Represents file data to set/copy on clients'
+ data = etree.SubElement(policysetting, 'data')
+ file_properties = etree.SubElement(data, 'file_properties')
+ source = etree.SubElement(file_properties, 'source')
+ source.text = source_file
+ target = etree.SubElement(file_properties, 'target')
+ target.text = os.path.join(self.tempdir, 'test.target')
+ user = etree.SubElement(file_properties, 'user')
+ user.text = pwd.getpwuid(os.getuid()).pw_name
+ group = etree.SubElement(file_properties, 'group')
+ group.text = grp.getgrgid(os.getgid()).gr_name
+
+ # Request permissions of 755
+ permissions = etree.SubElement(file_properties, 'permissions')
+ permissions.set('type', 'user')
+ etree.SubElement(permissions, 'read')
+ etree.SubElement(permissions, 'write')
+ etree.SubElement(permissions, 'execute')
+ permissions = etree.SubElement(file_properties, 'permissions')
+ permissions.set('type', 'group')
+ etree.SubElement(permissions, 'read')
+ etree.SubElement(permissions, 'execute')
+ permissions = etree.SubElement(file_properties, 'permissions')
+ permissions.set('type', 'other')
+ etree.SubElement(permissions, 'read')
+ etree.SubElement(permissions, 'execute')
+
+ ret = stage_file(vgp_xml, etree.tostring(stage, 'utf-8'))
+ self.assertTrue(ret, 'Could not create the target %s' % vgp_xml)
+
+ (result, out, err) = self.runsublevelcmd("gpo", ("manage",
+ "files", "list"),
+ self.gpo_guid, "-H",
+ "ldap://%s" %
+ os.environ["SERVER"],
+ "-U%s%%%s" %
+ (os.environ["USERNAME"],
+ os.environ["PASSWORD"]))
+ self.assertIn(target.text, out, 'The test entry was not found!')
+ self.assertIn('-rwxr-xr-x', out,
+ 'The test entry permissions were not found')
+
+ # Unstage the manifest.xml file
+ unstage_file(vgp_xml)
+
+ def test_files_add(self):
+ lp = LoadParm()
+ lp.load(os.environ['SERVERCONFFILE'])
+ local_path = lp.get('path', 'sysvol')
+ sysvol_source = os.path.join(local_path, lp.get('realm').lower(),
+ 'Policies', self.gpo_guid, 'Machine/VGP',
+ 'VTLA/Unix/Files/test.source')
+ source_file = os.path.join(self.tempdir, 'test.source')
+ source_data = '#!/bin/sh\necho hello world'
+ with open(source_file, 'w') as w:
+ w.write(source_data)
+ target_file = os.path.join(self.tempdir, 'test.target')
+ user = pwd.getpwuid(os.getuid()).pw_name
+ group = grp.getgrgid(os.getgid()).gr_name
+ before_vers = gpt_ini_version(self.gpo_guid)
+ (result, out, err) = self.runsublevelcmd("gpo", ("manage",
+ "files", "add"),
+ self.gpo_guid,
+ source_file,
+ target_file,
+ user, group,
+ '755', "-H",
+ "ldap://%s" %
+ os.environ["SERVER"],
+ "-U%s%%%s" %
+ (os.environ["USERNAME"],
+ os.environ["PASSWORD"]))
+ self.assertCmdSuccess(result, out, err, 'File add failed')
+ self.assertIn(source_data, open(sysvol_source, 'r').read(),
+ 'Failed to find the source file on the sysvol')
+ after_vers = gpt_ini_version(self.gpo_guid)
+ self.assertGreater(after_vers, before_vers, 'GPT.INI was not updated')
+
+ (result, out, err) = self.runsublevelcmd("gpo", ("manage",
+ "files", "list"),
+ self.gpo_guid, "-H",
+ "ldap://%s" %
+ os.environ["SERVER"],
+ "-U%s%%%s" %
+ (os.environ["USERNAME"],
+ os.environ["PASSWORD"]))
+ self.assertIn(target_file, out, 'The test entry was not found!')
+ self.assertIn('-rwxr-xr-x', out,
+ 'The test entry permissions were not found')
+
+ os.unlink(source_file)
+
+ before_vers = after_vers
+ (result, out, err) = self.runsublevelcmd("gpo", ("manage",
+ "files", "remove"),
+ self.gpo_guid,
+ target_file, "-H",
+ "ldap://%s" %
+ os.environ["SERVER"],
+ "-U%s%%%s" %
+ (os.environ["USERNAME"],
+ os.environ["PASSWORD"]))
+ self.assertCmdSuccess(result, out, err, 'File remove failed')
+ after_vers = gpt_ini_version(self.gpo_guid)
+ self.assertGreater(after_vers, before_vers, 'GPT.INI was not updated')
+
+ (result, out, err) = self.runsublevelcmd("gpo", ("manage",
+ "files", "list"),
+ self.gpo_guid, "-H",
+ "ldap://%s" %
+ os.environ["SERVER"],
+ "-U%s%%%s" %
+ (os.environ["USERNAME"],
+ os.environ["PASSWORD"]))
+ self.assertNotIn(target_file, out, 'The test entry was still found!')
+
+ def test_vgp_openssh_list(self):
+ lp = LoadParm()
+ lp.load(os.environ['SERVERCONFFILE'])
+ local_path = lp.get('path', 'sysvol')
+ vgp_xml = os.path.join(local_path, lp.get('realm').lower(), 'Policies',
+ self.gpo_guid, 'Machine/VGP/VTLA/SshCfg',
+ 'SshD/manifest.xml')
+
+ stage = etree.Element('vgppolicy')
+ policysetting = etree.SubElement(stage, 'policysetting')
+ pv = etree.SubElement(policysetting, 'version')
+ pv.text = '1'
+ name = etree.SubElement(policysetting, 'name')
+ name.text = 'Configuration File'
+ description = etree.SubElement(policysetting, 'description')
+ description.text = 'Represents Unix configuration file settings'
+ apply_mode = etree.SubElement(policysetting, 'apply_mode')
+ apply_mode.text = 'merge'
+ data = etree.SubElement(policysetting, 'data')
+ configfile = etree.SubElement(data, 'configfile')
+ etree.SubElement(configfile, 'filename')
+ configsection = etree.SubElement(configfile, 'configsection')
+ etree.SubElement(configsection, 'sectionname')
+ opt = etree.SubElement(configsection, 'keyvaluepair')
+ key = etree.SubElement(opt, 'key')
+ key.text = 'KerberosAuthentication'
+ value = etree.SubElement(opt, 'value')
+ value.text = 'Yes'
+ ret = stage_file(vgp_xml, etree.tostring(stage, 'utf-8'))
+ self.assertTrue(ret, 'Could not create the target %s' % vgp_xml)
+
+ openssh = 'KerberosAuthentication Yes'
+ (result, out, err) = self.runsublevelcmd("gpo", ("manage",
+ "openssh", "list"),
+ self.gpo_guid, "-H",
+ "ldap://%s" %
+ os.environ["SERVER"],
+ "-U%s%%%s" %
+ (os.environ["USERNAME"],
+ os.environ["PASSWORD"]))
+ self.assertIn(openssh, out, 'The test entry was not found!')
+
+ # Unstage the manifest.xml file
+ unstage_file(vgp_xml)
+
+ def test_vgp_openssh_set(self):
+ before_vers = gpt_ini_version(self.gpo_guid)
+ (result, out, err) = self.runsublevelcmd("gpo", ("manage",
+ "openssh", "set"),
+ self.gpo_guid,
+ "KerberosAuthentication",
+ "Yes", "-H",
+ "ldap://%s" %
+ os.environ["SERVER"],
+ "-U%s%%%s" %
+ (os.environ["USERNAME"],
+ os.environ["PASSWORD"]))
+ self.assertCmdSuccess(result, out, err, 'OpenSSH set failed')
+ after_vers = gpt_ini_version(self.gpo_guid)
+ self.assertGreater(after_vers, before_vers, 'GPT.INI was not updated')
+
+ openssh = 'KerberosAuthentication Yes'
+ (result, out, err) = self.runsublevelcmd("gpo", ("manage",
+ "openssh", "list"),
+ self.gpo_guid, "-H",
+ "ldap://%s" %
+ os.environ["SERVER"],
+ "-U%s%%%s" %
+ (os.environ["USERNAME"],
+ os.environ["PASSWORD"]))
+ self.assertIn(openssh, out, 'The test entry was not found!')
+
+ before_vers = after_vers
+ (result, out, err) = self.runsublevelcmd("gpo", ("manage",
+ "openssh", "set"),
+ self.gpo_guid,
+ "KerberosAuthentication", "-H",
+ "ldap://%s" %
+ os.environ["SERVER"],
+ "-U%s%%%s" %
+ (os.environ["USERNAME"],
+ os.environ["PASSWORD"]))
+ self.assertCmdSuccess(result, out, err, 'OpenSSH unset failed')
+ after_vers = gpt_ini_version(self.gpo_guid)
+ self.assertGreater(after_vers, before_vers, 'GPT.INI was not updated')
+
+ (result, out, err) = self.runsublevelcmd("gpo", ("manage",
+ "openssh", "list"),
+ self.gpo_guid, "-H",
+ "ldap://%s" %
+ os.environ["SERVER"],
+ "-U%s%%%s" %
+ (os.environ["USERNAME"],
+ os.environ["PASSWORD"]))
+ self.assertNotIn(openssh, out, 'The test entry was still found!')
+
+ def test_startup_script_add(self):
+ lp = LoadParm()
+ fname = None
+ before_vers = gpt_ini_version(self.gpo_guid)
+ with NamedTemporaryFile() as f:
+ fname = os.path.basename(f.name)
+ f.write(b'#!/bin/sh\necho $@ hello world')
+ f.flush()
+ (result, out, err) = self.runsublevelcmd("gpo", ("manage",
+ "scripts", "startup",
+ "add"), self.gpo_guid,
+ f.name, "'-n'", "-H",
+ "ldap://%s" %
+ os.environ["SERVER"],
+ "-U%s%%%s" %
+ (os.environ["USERNAME"],
+ os.environ["PASSWORD"]))
+ self.assertCmdSuccess(result, out, err, 'Script add failed')
+ after_vers = gpt_ini_version(self.gpo_guid)
+ self.assertGreater(after_vers, before_vers, 'GPT.INI was not updated')
+
+ script_path = '\\'.join(['\\', lp.get('realm').lower(), 'Policies',
+ self.gpo_guid, 'MACHINE\\VGP\\VTLA\\Unix',
+ 'Scripts\\Startup', fname])
+ entry = '@reboot root %s -n' % script_path
+ (result, out, err) = self.runsublevelcmd("gpo", ("manage", "scripts",
+ "startup", "list"),
+ self.gpo_guid, "-H",
+ "ldap://%s" %
+ os.environ["SERVER"],
+ "-U%s%%%s" %
+ (os.environ["USERNAME"],
+ os.environ["PASSWORD"]))
+ self.assertIn(entry, out, 'The test entry was not found!')
+ local_path = lp.get('path', 'sysvol')
+ local_script_path = os.path.join(local_path, lp.get('realm').lower(),
+ 'Policies', self.gpo_guid,
+ 'Machine/VGP/VTLA/Unix',
+ 'Scripts/Startup', fname)
+ self.assertTrue(os.path.exists(local_script_path),
+ 'The test script was not uploaded to the sysvol')
+
+ before_vers = after_vers
+ (result, out, err) = self.runsublevelcmd("gpo", ("manage",
+ "scripts", "startup",
+ "remove"), self.gpo_guid,
+ f.name, "-H",
+ "ldap://%s" %
+ os.environ["SERVER"],
+ "-U%s%%%s" %
+ (os.environ["USERNAME"],
+ os.environ["PASSWORD"]))
+ self.assertCmdSuccess(result, out, err, 'Script remove failed')
+ after_vers = gpt_ini_version(self.gpo_guid)
+ self.assertGreater(after_vers, before_vers, 'GPT.INI was not updated')
+
+ (result, out, err) = self.runsublevelcmd("gpo", ("manage", "scripts",
+ "startup", "list"),
+ self.gpo_guid, "-H",
+ "ldap://%s" %
+ os.environ["SERVER"],
+ "-U%s%%%s" %
+ (os.environ["USERNAME"],
+ os.environ["PASSWORD"]))
+ self.assertNotIn(entry, out, 'The test entry was still found!')
+
+ def test_startup_script_list(self):
+ lp = LoadParm()
+ lp.load(os.environ['SERVERCONFFILE'])
+ local_path = lp.get('path', 'sysvol')
+ vgp_xml = os.path.join(local_path, lp.get('realm').lower(), 'Policies',
+ self.gpo_guid, 'Machine/VGP/VTLA/Unix',
+ 'Scripts/Startup/manifest.xml')
+ stage = etree.Element('vgppolicy')
+ policysetting = etree.SubElement(stage, 'policysetting')
+ pv = etree.SubElement(policysetting, 'version')
+ pv.text = '1'
+ name = etree.SubElement(policysetting, 'name')
+ name.text = 'Unix Scripts'
+ description = etree.SubElement(policysetting, 'description')
+ description.text = 'Represents Unix scripts to run on Group Policy clients'
+ data = etree.SubElement(policysetting, 'data')
+ listelement = etree.SubElement(data, 'listelement')
+ script = etree.SubElement(listelement, 'script')
+ script.text = 'test.sh'
+ parameters = etree.SubElement(listelement, 'parameters')
+ parameters.text = '-e'
+ ret = stage_file(vgp_xml, etree.tostring(stage, 'utf-8'))
+ self.assertTrue(ret, 'Could not create the target %s' % vgp_xml)
+
+ script_path = '\\'.join(['\\', lp.get('realm').lower(), 'Policies',
+ self.gpo_guid, 'MACHINE\\VGP\\VTLA\\Unix',
+ 'Scripts\\Startup', script.text])
+ entry = '@reboot root %s %s' % (script_path, parameters.text)
+ (result, out, err) = self.runsublevelcmd("gpo", ("manage", "scripts",
+ "startup", "list"),
+ self.gpo_guid, "-H",
+ "ldap://%s" %
+ os.environ["SERVER"],
+ "-U%s%%%s" %
+ (os.environ["USERNAME"],
+ os.environ["PASSWORD"]))
+ self.assertIn(entry, out, 'The test entry was not found!')
+
+ # Unstage the manifest.xml file
+ unstage_file(vgp_xml)
+
+ def test_vgp_motd_set(self):
+ text = 'This is the message of the day'
+ msg = '"%s\n"' % text
+ before_vers = gpt_ini_version(self.gpo_guid)
+ (result, out, err) = self.runsublevelcmd("gpo", ("manage",
+ "motd", "set"),
+ self.gpo_guid,
+ msg, "-H",
+ "ldap://%s" %
+ os.environ["SERVER"],
+ "-U%s%%%s" %
+ (os.environ["USERNAME"],
+ os.environ["PASSWORD"]))
+ self.assertCmdSuccess(result, out, err, 'MOTD set failed')
+ after_vers = gpt_ini_version(self.gpo_guid)
+ self.assertGreater(after_vers, before_vers, 'GPT.INI was not updated')
+
+ (result, out, err) = self.runsublevelcmd("gpo", ("manage",
+ "motd", "list"),
+ self.gpo_guid, "-H",
+ "ldap://%s" %
+ os.environ["SERVER"],
+ "-U%s%%%s" %
+ (os.environ["USERNAME"],
+ os.environ["PASSWORD"]))
+ self.assertIn(text, out, 'The test entry was not found!')
+
+ before_vers = after_vers
+ (result, out, err) = self.runsublevelcmd("gpo", ("manage",
+ "motd", "set"),
+ self.gpo_guid, "-H",
+ "ldap://%s" %
+ os.environ["SERVER"],
+ "-U%s%%%s" %
+ (os.environ["USERNAME"],
+ os.environ["PASSWORD"]))
+ self.assertCmdSuccess(result, out, err, 'MOTD unset failed')
+ after_vers = gpt_ini_version(self.gpo_guid)
+ self.assertGreater(after_vers, before_vers, 'GPT.INI was not updated')
+
+ (result, out, err) = self.runsublevelcmd("gpo", ("manage",
+ "motd", "list"),
+ self.gpo_guid, "-H",
+ "ldap://%s" %
+ os.environ["SERVER"],
+ "-U%s%%%s" %
+ (os.environ["USERNAME"],
+ os.environ["PASSWORD"]))
+ self.assertNotIn(text, out, 'The test entry was still found!')
+
+ def test_vgp_motd(self):
+ lp = LoadParm()
+ lp.load(os.environ['SERVERCONFFILE'])
+ local_path = lp.get('path', 'sysvol')
+ vgp_xml = os.path.join(local_path, lp.get('realm').lower(), 'Policies',
+ self.gpo_guid, 'Machine/VGP/VTLA/Unix',
+ 'MOTD/manifest.xml')
+
+ stage = etree.Element('vgppolicy')
+ policysetting = etree.SubElement(stage, 'policysetting')
+ pv = etree.SubElement(policysetting, 'version')
+ pv.text = '1'
+ name = etree.SubElement(policysetting, 'name')
+ name.text = 'Text File'
+ description = etree.SubElement(policysetting, 'description')
+ description.text = 'Represents a Generic Text File'
+ apply_mode = etree.SubElement(policysetting, 'apply_mode')
+ apply_mode.text = 'replace'
+ data = etree.SubElement(policysetting, 'data')
+ filename = etree.SubElement(data, 'filename')
+ filename.text = 'motd'
+ text = etree.SubElement(data, 'text')
+ text.text = 'This is a message of the day'
+ ret = stage_file(vgp_xml, etree.tostring(stage, 'utf-8'))
+ self.assertTrue(ret, 'Could not create the target %s' % vgp_xml)
+
+ (result, out, err) = self.runsublevelcmd("gpo", ("manage",
+ "motd", "list"),
+ self.gpo_guid, "-H",
+ "ldap://%s" %
+ os.environ["SERVER"],
+ "-U%s%%%s" %
+ (os.environ["USERNAME"],
+ os.environ["PASSWORD"]))
+ self.assertIn(text.text, out, 'The test entry was not found!')
+
+ # Unstage the manifest.xml file
+ unstage_file(vgp_xml)
+
+ def test_vgp_issue_list(self):
+ lp = LoadParm()
+ lp.load(os.environ['SERVERCONFFILE'])
+ local_path = lp.get('path', 'sysvol')
+ vgp_xml = os.path.join(local_path, lp.get('realm').lower(), 'Policies',
+ self.gpo_guid, 'Machine/VGP/VTLA/Unix',
+ 'Issue/manifest.xml')
+
+ stage = etree.Element('vgppolicy')
+ policysetting = etree.SubElement(stage, 'policysetting')
+ pv = etree.SubElement(policysetting, 'version')
+ pv.text = '1'
+ name = etree.SubElement(policysetting, 'name')
+ name.text = 'Text File'
+ description = etree.SubElement(policysetting, 'description')
+ description.text = 'Represents a Generic Text File'
+ apply_mode = etree.SubElement(policysetting, 'apply_mode')
+ apply_mode.text = 'replace'
+ data = etree.SubElement(policysetting, 'data')
+ filename = etree.SubElement(data, 'filename')
+ filename.text = 'issue'
+ text = etree.SubElement(data, 'text')
+ text.text = 'Welcome to Samba!'
+ ret = stage_file(vgp_xml, etree.tostring(stage, 'utf-8'))
+ self.assertTrue(ret, 'Could not create the target %s' % vgp_xml)
+
+ (result, out, err) = self.runsublevelcmd("gpo", ("manage",
+ "issue", "list"),
+ self.gpo_guid, "-H",
+ "ldap://%s" %
+ os.environ["SERVER"],
+ "-U%s%%%s" %
+ (os.environ["USERNAME"],
+ os.environ["PASSWORD"]))
+ self.assertIn(text.text, out, 'The test entry was not found!')
+
+ # Unstage the manifest.xml file
+ unstage_file(vgp_xml)
+
+ def test_vgp_issue_set(self):
+ text = 'Welcome to Samba!'
+ msg = '"%s\n"' % text
+ before_vers = gpt_ini_version(self.gpo_guid)
+ (result, out, err) = self.runsublevelcmd("gpo", ("manage",
+ "issue", "set"),
+ self.gpo_guid,
+ msg, "-H",
+ "ldap://%s" %
+ os.environ["SERVER"],
+ "-U%s%%%s" %
+ (os.environ["USERNAME"],
+ os.environ["PASSWORD"]))
+ self.assertCmdSuccess(result, out, err, 'Issue set failed')
+ after_vers = gpt_ini_version(self.gpo_guid)
+ self.assertGreater(after_vers, before_vers, 'GPT.INI was not updated')
+
+ (result, out, err) = self.runsublevelcmd("gpo", ("manage",
+ "issue", "list"),
+ self.gpo_guid, "-H",
+ "ldap://%s" %
+ os.environ["SERVER"],
+ "-U%s%%%s" %
+ (os.environ["USERNAME"],
+ os.environ["PASSWORD"]))
+ self.assertIn(text, out, 'The test entry was not found!')
+
+ before_vers = after_vers
+ (result, out, err) = self.runsublevelcmd("gpo", ("manage",
+ "issue", "set"),
+ self.gpo_guid, "-H",
+ "ldap://%s" %
+ os.environ["SERVER"],
+ "-U%s%%%s" %
+ (os.environ["USERNAME"],
+ os.environ["PASSWORD"]))
+ self.assertCmdSuccess(result, out, err, 'Issue unset failed')
+ after_vers = gpt_ini_version(self.gpo_guid)
+ self.assertGreater(after_vers, before_vers, 'GPT.INI was not updated')
+
+ (result, out, err) = self.runsublevelcmd("gpo", ("manage",
+ "issue", "list"),
+ self.gpo_guid, "-H",
+ "ldap://%s" %
+ os.environ["SERVER"],
+ "-U%s%%%s" %
+ (os.environ["USERNAME"],
+ os.environ["PASSWORD"]))
+ self.assertNotIn(text, out, 'The test entry was still found!')
+
+ def test_load_show_remove(self):
+ before_vers = gpt_ini_version(self.gpo_guid)
+ with NamedTemporaryFile() as f:
+ f.write(gpo_load_json)
+ f.flush()
+ (result, out, err) = self.runsubcmd("gpo", "load",
+ self.gpo_guid,
+ "--content=%s" % f.name,
+ "--machine-ext-name=%s" %
+ ext_guids[0],
+ "--user-ext-name=%s" %
+ ext_guids[1],
+ "-H", "ldap://%s" %
+ os.environ["SERVER"],
+ "-U%s%%%s" %
+ (os.environ["USERNAME"],
+ os.environ["PASSWORD"]))
+ self.assertCmdSuccess(result, out, err, 'Loading policy failed')
+ after_vers = gpt_ini_version(self.gpo_guid)
+ self.assertGreater(after_vers, before_vers, 'GPT.INI was not updated')
+
+ before_vers = after_vers
+ # Write the default registry extension
+ with NamedTemporaryFile() as f:
+ f.write(b'[]') # Intentionally empty policy
+ f.flush()
+ # Load an empty policy, taking the default client extension
+ (result, out, err) = self.runsubcmd("gpo", "load",
+ self.gpo_guid,
+ "--content=%s" % f.name,
+ "-H", "ldap://%s" %
+ os.environ["SERVER"],
+ "-U%s%%%s" %
+ (os.environ["USERNAME"],
+ os.environ["PASSWORD"]))
+ self.assertCmdSuccess(result, out, err, 'Loading policy failed')
+ after_vers = gpt_ini_version(self.gpo_guid)
+ self.assertEqual(after_vers, before_vers,
+ 'GPT.INI changed on empty merge')
+
+ (result, out, err) = self.runsubcmd("gpo", "show", self.gpo_guid, "-H",
+ "ldap://%s" % os.environ["SERVER"])
+ self.assertCmdSuccess(result, out, err, 'Failed to fetch gpos')
+ self.assertIn('homepage', out, 'Homepage policy not loaded')
+ self.assertIn('samba.org', out, 'Homepage policy not loaded')
+ self.assertIn(ext_guids[0], out, 'Machine extension not loaded')
+ self.assertIn(ext_guids[1], out, 'User extension not loaded')
+ self.assertIn('{35378eac-683f-11d2-a89a-00c04fbbcfa2}', out,
+ 'Default extension not loaded')
+ toolbar_data = '"valuename": "IEToolbar",\n "class": "USER",' + \
+ '\n "type": "REG_BINARY",' + \
+ '\n "data": [\n 0\n ]'
+ self.assertIn(toolbar_data, out, 'Toolbar policy not loaded')
+ restrict_data = '"valuename": "RestrictImplicitTextCollection",' + \
+ '\n "class": "USER",' + \
+ '\n "type": "REG_DWORD",\n "data": 1\n'
+ self.assertIn(restrict_data, out, 'Restrict policy not loaded')
+ ext_data = '" \\"key\\": \\"value\\"",'
+ self.assertIn(ext_data, out, 'Extension policy not loaded')
+
+ before_vers = after_vers
+ with NamedTemporaryFile() as f:
+ f.write(gpo_remove_json)
+ f.flush()
+ (result, out, err) = self.runsubcmd("gpo", "remove",
+ self.gpo_guid,
+ "--content=%s" % f.name,
+ "--machine-ext-name=%s" %
+ ext_guids[0],
+ "--user-ext-name=%s" %
+ ext_guids[1],
+ "-H", "ldap://%s" %
+ os.environ["SERVER"],
+ "-U%s%%%s" %
+ (os.environ["USERNAME"],
+ os.environ["PASSWORD"]))
+ self.assertCmdSuccess(result, out, err, 'Removing policy failed')
+ after_vers = gpt_ini_version(self.gpo_guid)
+ self.assertGreater(after_vers, before_vers, 'GPT.INI was not updated')
+
+ (result, out, err) = self.runsubcmd("gpo", "show", self.gpo_guid, "-H",
+ "ldap://%s" % os.environ["SERVER"])
+ self.assertCmdSuccess(result, out, err, 'Failed to fetch gpos')
+ self.assertNotIn('samba.org', out, 'Homepage policy not removed')
+ self.assertNotIn(ext_guids[0], out, 'Machine extension not unloaded')
+ self.assertNotIn(ext_guids[1], out, 'User extension not unloaded')
+
+ def test_cse_register_unregister_list(self):
+ with NamedTemporaryFile() as f:
+ (result, out, err) = self.runsublevelcmd("gpo", ("cse",
+ "register"),
+ f.name, 'gp_test_ext',
+ '--machine')
+ self.assertCmdSuccess(result, out, err, 'CSE register failed')
+
+ (result, out, err) = self.runsublevelcmd("gpo", ("cse",
+ "list"))
+ self.assertIn(f.name, out, 'The test cse was not found')
+ self.assertIn('ProcessGroupPolicy : gp_test_ext', out,
+ 'The test cse was not found')
+ self.assertIn('MachinePolicy : True', out,
+ 'The test cse was not enabled')
+ self.assertIn('UserPolicy : False', out,
+ 'The test cse should not have User policy enabled')
+ cse_ext = re.findall(r'^UniqueGUID\s+:\s+(.*)', out)
+ self.assertEqual(len(cse_ext), 1,
+ 'The test cse GUID was not found')
+ cse_ext = cse_ext[0]
+ self.assertTrue(check_guid(cse_ext),
+ 'The test cse GUID was not formatted correctly')
+
+ (result, out, err) = self.runsublevelcmd("gpo", ("cse",
+ "unregister"),
+ cse_ext)
+ self.assertCmdSuccess(result, out, err, 'CSE unregister failed')
+
+ (result, out, err) = self.runsublevelcmd("gpo", ("cse",
+ "list"))
+ self.assertNotIn(f.name, out, 'The test cse was still found')
+
+ def setUp(self):
+ """set up a temporary GPO to work with"""
+ super().setUp()
+ (result, out, err) = self.runsubcmd("gpo", "create", self.gpo_name,
+ "-H", "ldap://%s" % os.environ["SERVER"],
+ "-U%s%%%s" % (os.environ["USERNAME"], os.environ["PASSWORD"]),
+ "--tmpdir", self.tempdir)
+ self.assertCmdSuccess(result, out, err, "Ensuring gpo created successfully")
+ shutil.rmtree(os.path.join(self.tempdir, "policy"))
+ try:
+ self.gpo_guid = "{%s}" % out.split("{")[1].split("}")[0]
+ except IndexError:
+ self.fail("Failed to find GUID in output: %s" % out)
+
+ self.backup_path = os.path.join(samba.source_tree_topdir(), 'source4',
+ 'selftest', 'provisions',
+ 'generalized-gpo-backup')
+
+ self.entity_file = os.path.join(self.backup_path, 'entities')
+
+ def tearDown(self):
+ """remove the temporary GPO to work with"""
+ (result, out, err) = self.runsubcmd("gpo", "del", self.gpo_guid, "-H", "ldap://%s" % os.environ["SERVER"], "-U%s%%%s" % (os.environ["USERNAME"], os.environ["PASSWORD"]))
+ self.assertCmdSuccess(result, out, err, "Ensuring gpo deleted successfully")
+ super().tearDown()
diff --git a/python/samba/tests/samba_tool/gpo_exts.py b/python/samba/tests/samba_tool/gpo_exts.py
new file mode 100644
index 0000000..e7a24b0
--- /dev/null
+++ b/python/samba/tests/samba_tool/gpo_exts.py
@@ -0,0 +1,202 @@
+# Unix SMB/CIFS implementation.
+# Copyright (C) David Mulder 2021
+#
+# based on gpo.py:
+# Copyright (C) Andrew Bartlett 2012
+#
+# 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 os
+from samba.tests.samba_tool.base import SambaToolCmdTest
+import shutil
+from samba.param import LoadParm
+from samba.tests.gpo import stage_file, unstage_file
+import xml.etree.ElementTree as etree
+
+class GpoCmdTestCase(SambaToolCmdTest):
+ """Tests for samba-tool time subcommands"""
+
+ gpo_name = "testgpo"
+
+ def test_vgp_access_list(self):
+ lp = LoadParm()
+ lp.load(os.environ['SERVERCONFFILE'])
+ local_path = lp.get('path', 'sysvol')
+ vgp_xml = os.path.join(local_path, lp.get('realm').lower(), 'Policies',
+ self.gpo_guid, 'Machine/VGP/VTLA/VAS',
+ 'HostAccessControl/Allow/manifest.xml')
+
+ stage = etree.Element('vgppolicy')
+ policysetting = etree.SubElement(stage, 'policysetting')
+ pv = etree.SubElement(policysetting, 'version')
+ pv.text = '1'
+ name = etree.SubElement(policysetting, 'name')
+ name.text = 'Host Access Control'
+ description = etree.SubElement(policysetting, 'description')
+ description.text = 'Represents host access control data (pam_access)'
+ apply_mode = etree.SubElement(policysetting, 'apply_mode')
+ apply_mode.text = 'merge'
+ data = etree.SubElement(policysetting, 'data')
+ listelement = etree.SubElement(data, 'listelement')
+ etype = etree.SubElement(listelement, 'type')
+ etype.text = 'USER'
+ entry = etree.SubElement(listelement, 'entry')
+ entry.text = 'goodguy@%s' % lp.get('realm').lower()
+ adobject = etree.SubElement(listelement, 'adobject')
+ name = etree.SubElement(adobject, 'name')
+ name.text = 'goodguy'
+ domain = etree.SubElement(adobject, 'domain')
+ domain.text = lp.get('realm').lower()
+ etype = etree.SubElement(adobject, 'type')
+ etype.text = 'user'
+ groupattr = etree.SubElement(data, 'groupattr')
+ groupattr.text = 'samAccountName'
+ listelement = etree.SubElement(data, 'listelement')
+ etype = etree.SubElement(listelement, 'type')
+ etype.text = 'GROUP'
+ entry = etree.SubElement(listelement, 'entry')
+ entry.text = '%s\\goodguys' % lp.get('realm').lower()
+ adobject = etree.SubElement(listelement, 'adobject')
+ name = etree.SubElement(adobject, 'name')
+ name.text = 'goodguys'
+ domain = etree.SubElement(adobject, 'domain')
+ domain.text = lp.get('realm').lower()
+ etype = etree.SubElement(adobject, 'type')
+ etype.text = 'group'
+ ret = stage_file(vgp_xml, etree.tostring(stage, 'utf-8'))
+ self.assertTrue(ret, 'Could not create the target %s' % vgp_xml)
+
+ uentry = '+:%s\\goodguy:ALL' % domain.text
+ gentry = '+:%s\\goodguys:ALL' % domain.text
+ (result, out, err) = self.runsublevelcmd("gpo", ("manage",
+ "access", "list"),
+ self.gpo_guid, "-H",
+ "ldap://%s" %
+ os.environ["SERVER"],
+ "-U%s%%%s" %
+ (os.environ["USERNAME"],
+ os.environ["PASSWORD"]))
+ self.assertIn(uentry, out, 'The test entry was not found!')
+ self.assertIn(gentry, out, 'The test entry was not found!')
+
+ # Unstage the manifest.xml file
+ unstage_file(vgp_xml)
+
+ def test_vgp_access_add(self):
+ lp = LoadParm()
+ lp.load(os.environ['SERVERCONFFILE'])
+
+ (result, out, err) = self.runsublevelcmd("gpo", ("manage",
+ "access", "add"),
+ self.gpo_guid,
+ "allow", self.test_user,
+ lp.get('realm').lower(),
+ "-H", "ldap://%s" %
+ os.environ["SERVER"],
+ "-U%s%%%s" %
+ (os.environ["USERNAME"],
+ os.environ["PASSWORD"]))
+ self.assertCmdSuccess(result, out, err, 'Access add failed')
+
+ (result, out, err) = self.runsublevelcmd("gpo", ("manage",
+ "access", "add"),
+ self.gpo_guid,
+ "deny", self.test_group,
+ lp.get('realm').lower(),
+ "-H", "ldap://%s" %
+ os.environ["SERVER"],
+ "-U%s%%%s" %
+ (os.environ["USERNAME"],
+ os.environ["PASSWORD"]))
+ self.assertCmdSuccess(result, out, err, 'Access add failed')
+
+ allow_entry = '+:%s\\%s:ALL' % (lp.get('realm').lower(), self.test_user)
+ deny_entry = '-:%s\\%s:ALL' % (lp.get('realm').lower(), self.test_group)
+ (result, out, err) = self.runsublevelcmd("gpo", ("manage",
+ "access", "list"),
+ self.gpo_guid, "-H",
+ "ldap://%s" %
+ os.environ["SERVER"],
+ "-U%s%%%s" %
+ (os.environ["USERNAME"],
+ os.environ["PASSWORD"]))
+ self.assertIn(allow_entry, out, 'The test entry was not found!')
+ self.assertIn(deny_entry, out, 'The test entry was not found!')
+
+ (result, out, err) = self.runsublevelcmd("gpo", ("manage",
+ "access", "remove"),
+ self.gpo_guid,
+ "allow", self.test_user,
+ lp.get('realm').lower(),
+ "-H", "ldap://%s" %
+ os.environ["SERVER"],
+ "-U%s%%%s" %
+ (os.environ["USERNAME"],
+ os.environ["PASSWORD"]))
+ self.assertCmdSuccess(result, out, err, 'Access remove failed')
+ (result, out, err) = self.runsublevelcmd("gpo", ("manage",
+ "access", "remove"),
+ self.gpo_guid,
+ "deny", self.test_group,
+ lp.get('realm').lower(),
+ "-H", "ldap://%s" %
+ os.environ["SERVER"],
+ "-U%s%%%s" %
+ (os.environ["USERNAME"],
+ os.environ["PASSWORD"]))
+ self.assertCmdSuccess(result, out, err, 'Access remove failed')
+
+ (result, out, err) = self.runsublevelcmd("gpo", ("manage",
+ "access", "list"),
+ self.gpo_guid, "-H",
+ "ldap://%s" %
+ os.environ["SERVER"],
+ "-U%s%%%s" %
+ (os.environ["USERNAME"],
+ os.environ["PASSWORD"]))
+ self.assertNotIn(allow_entry, out, 'The test entry was still found!')
+ self.assertNotIn(deny_entry, out, 'The test entry was still found!')
+
+ def setUp(self):
+ """set up a temporary GPO to work with"""
+ super().setUp()
+ (result, out, err) = self.runsubcmd("gpo", "create", self.gpo_name,
+ "-H", "ldap://%s" % os.environ["SERVER"],
+ "-U%s%%%s" % (os.environ["USERNAME"], os.environ["PASSWORD"]),
+ "--tmpdir", self.tempdir)
+ self.assertCmdSuccess(result, out, err, "Ensuring gpo created successfully")
+ shutil.rmtree(os.path.join(self.tempdir, "policy"))
+ try:
+ self.gpo_guid = "{%s}" % out.split("{")[1].split("}")[0]
+ except IndexError:
+ self.fail("Failed to find GUID in output: %s" % out)
+
+ self.test_user = 'testuser'
+ (result, out, err) = self.runsubcmd("user", "add", self.test_user,
+ "--random-password")
+ self.assertCmdSuccess(result, out, err, 'User creation failed')
+ self.test_group = 'testgroup'
+ (result, out, err) = self.runsubcmd("group", "add", self.test_group)
+ self.assertCmdSuccess(result, out, err, 'Group creation failed')
+
+ def tearDown(self):
+ """remove the temporary GPO to work with"""
+ (result, out, err) = self.runsubcmd("gpo", "del", self.gpo_guid, "-H", "ldap://%s" % os.environ["SERVER"], "-U%s%%%s" % (os.environ["USERNAME"], os.environ["PASSWORD"]))
+ self.assertCmdSuccess(result, out, err, "Ensuring gpo deleted successfully")
+ (result, out, err) = self.runsubcmd("user", "delete", self.test_user)
+ self.assertCmdSuccess(result, out, err, 'User delete failed')
+ (result, out, err) = self.runsubcmd("group", "delete", self.test_group)
+ self.assertCmdSuccess(result, out, err, 'Group delete failed')
+ super().tearDown()
diff --git a/python/samba/tests/samba_tool/group.py b/python/samba/tests/samba_tool/group.py
new file mode 100644
index 0000000..e7a660c
--- /dev/null
+++ b/python/samba/tests/samba_tool/group.py
@@ -0,0 +1,613 @@
+# Unix SMB/CIFS implementation.
+# Copyright (C) Michael Adam 2012
+#
+# 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 os
+import time
+import ldb
+from samba.tests.samba_tool.base import SambaToolCmdTest
+from samba import (
+ nttime2unix,
+ dsdb
+ )
+
+
+class GroupCmdTestCase(SambaToolCmdTest):
+ """Tests for samba-tool group subcommands"""
+ groups = []
+ samdb = None
+
+ def setUp(self):
+ super().setUp()
+ self.samdb = self.getSamDB("-H", "ldap://%s" % os.environ["DC_SERVER"],
+ "-U%s%%%s" % (os.environ["DC_USERNAME"], os.environ["DC_PASSWORD"]))
+ self.groups = []
+ self.groups.append(self._randomGroup({"name": "testgroup1"}))
+ self.groups.append(self._randomGroup({"name": "testgroup2"}))
+ self.groups.append(self._randomGroup({"name": "testgroup3"}))
+ self.groups.append(self._randomGroup({"name": "testgroup4"}))
+ self.groups.append(self._randomGroup({"name": "testgroup5 (with brackets)"}))
+ self.groups.append(self._randomPosixGroup({"name": "posixgroup1"}))
+ self.groups.append(self._randomPosixGroup({"name": "posixgroup2"}))
+ self.groups.append(self._randomPosixGroup({"name": "posixgroup3"}))
+ self.groups.append(self._randomPosixGroup({"name": "posixgroup4"}))
+ self.groups.append(self._randomPosixGroup({"name": "posixgroup5 (with brackets)"}))
+ self.groups.append(self._randomUnixGroup({"name": "unixgroup1"}))
+ self.groups.append(self._randomUnixGroup({"name": "unixgroup2"}))
+ self.groups.append(self._randomUnixGroup({"name": "unixgroup3"}))
+ self.groups.append(self._randomUnixGroup({"name": "unixgroup4"}))
+ self.groups.append(self._randomUnixGroup({"name": "unixgroup5 (with brackets)"}))
+
+ # setup the 12 groups and ensure they are correct
+ for group in self.groups:
+ (result, out, err) = group["createGroupFn"](group)
+
+ self.assertCmdSuccess(result, out, err)
+ self.assertEqual(err, "", "There shouldn't be any error message")
+
+ if 'unix' in group["name"]:
+ self.assertIn("Modified Group '%s' successfully"
+ % group["name"], out)
+ else:
+ self.assertIn("Added group %s" % group["name"], out)
+
+ group["checkGroupFn"](group)
+
+ found = self._find_group(group["name"])
+
+ self.assertIsNotNone(found)
+
+ self.assertEqual("%s" % found.get("name"), group["name"])
+ self.assertEqual("%s" % found.get("description"), group["description"])
+
+ def tearDown(self):
+ super().tearDown()
+ # clean up all the left over groups, just in case
+ for group in self.groups:
+ if self._find_group(group["name"]):
+ self.runsubcmd("group", "delete", group["name"])
+
+ def test_newgroup(self):
+ """This tests the "group add" and "group delete" commands"""
+ # try to add all the groups again, this should fail
+ for group in self.groups:
+ (result, out, err) = self._create_group(group)
+ self.assertCmdFail(result, "Succeeded to add existing group")
+ self.assertIn("LDAP error 68 LDAP_ENTRY_ALREADY_EXISTS", err)
+
+ # try to delete all the groups we just added
+ for group in self.groups:
+ (result, out, err) = self.runsubcmd("group", "delete", group["name"])
+ self.assertCmdSuccess(result, out, err,
+ "Failed to delete group '%s'" % group["name"])
+ found = self._find_group(group["name"])
+ self.assertIsNone(found,
+ "Deleted group '%s' still exists" % group["name"])
+
+ # test adding groups
+ for group in self.groups:
+ (result, out, err) = self.runsubcmd("group", "add", group["name"],
+ "--description=%s" % group["description"],
+ "-H", "ldap://%s" % os.environ["DC_SERVER"],
+ "-U%s%%%s" % (os.environ["DC_USERNAME"],
+ os.environ["DC_PASSWORD"]))
+
+ self.assertCmdSuccess(result, out, err)
+ self.assertEqual(err, "", "There shouldn't be any error message")
+ self.assertIn("Added group %s" % group["name"], out)
+
+ found = self._find_group(group["name"])
+
+ self.assertEqual("%s" % found.get("samaccountname"),
+ "%s" % group["name"])
+
+ def test_list(self):
+ (result, out, err) = self.runsubcmd("group", "list",
+ "-H", "ldap://%s" % os.environ["DC_SERVER"],
+ "-U%s%%%s" % (os.environ["DC_USERNAME"],
+ os.environ["DC_PASSWORD"]))
+ self.assertCmdSuccess(result, out, err, "Error running list")
+
+ search_filter = "(objectClass=group)"
+
+ grouplist = self.samdb.search(base=self.samdb.domain_dn(),
+ scope=ldb.SCOPE_SUBTREE,
+ expression=search_filter,
+ attrs=["samaccountname"])
+
+ self.assertTrue(len(grouplist) > 0, "no groups found in samdb")
+
+ for groupobj in grouplist:
+ name = str(groupobj.get("samaccountname", idx=0))
+ found = self.assertMatch(out, name,
+ "group '%s' not found" % name)
+
+ def test_list_verbose(self):
+ (result, out, err) = self.runsubcmd("group", "list", "--verbose",
+ "-H", "ldap://%s" % os.environ["DC_SERVER"],
+ "-U%s%%%s" % (os.environ["DC_USERNAME"],
+ os.environ["DC_PASSWORD"]))
+ self.assertCmdSuccess(result, out, err, "Error running list --verbose")
+
+ # use the output to build a dictionary, where key=group-name,
+ # value=num-members
+ output_memberships = {}
+
+ # split the output by line, skipping the first 2 header lines
+ group_lines = out.split('\n')[2:-1]
+ for line in group_lines:
+ # split line by column whitespace (but keep the group name together
+ # if it contains spaces)
+ values = line.split(" ")
+ name = values[0]
+ num_members = int(values[-1])
+ output_memberships[name] = num_members
+
+ # build up a similar dict using an LDAP search
+ search_filter = "(objectClass=group)"
+ grouplist = self.samdb.search(base=self.samdb.domain_dn(),
+ scope=ldb.SCOPE_SUBTREE,
+ expression=search_filter,
+ attrs=["samaccountname", "member"])
+ self.assertTrue(len(grouplist) > 0, "no groups found in samdb")
+
+ ldap_memberships = {}
+ for groupobj in grouplist:
+ name = str(groupobj.get("samaccountname", idx=0))
+ num_members = len(groupobj.get("member", default=[]))
+ ldap_memberships[name] = num_members
+
+ # check the command output matches LDAP
+ self.assertTrue(output_memberships == ldap_memberships,
+ "Command output doesn't match LDAP results.\n" +
+ "Command='%s'\nLDAP='%s'" %(output_memberships,
+ ldap_memberships))
+
+ def test_list_full_dn(self):
+ (result, out, err) = self.runsubcmd("group", "list", "--full-dn",
+ "-H", "ldap://%s" % os.environ["DC_SERVER"],
+ "-U%s%%%s" % (os.environ["DC_USERNAME"],
+ os.environ["DC_PASSWORD"]))
+ self.assertCmdSuccess(result, out, err, "Error running list")
+
+ search_filter = "(objectClass=group)"
+
+ grouplist = self.samdb.search(base=self.samdb.domain_dn(),
+ scope=ldb.SCOPE_SUBTREE,
+ expression=search_filter,
+ attrs=[])
+
+ self.assertTrue(len(grouplist) > 0, "no groups found in samdb")
+
+ for groupobj in grouplist:
+ name = str(groupobj.get("dn", idx=0))
+ found = self.assertMatch(out, name,
+ "group '%s' not found" % name)
+
+ def test_list_base_dn(self):
+ base_dn = "CN=Users"
+ (result, out, err) = self.runsubcmd("group", "list", "--base-dn", base_dn,
+ "-H", "ldap://%s" % os.environ["DC_SERVER"],
+ "-U%s%%%s" % (os.environ["DC_USERNAME"],
+ os.environ["DC_PASSWORD"]))
+ self.assertCmdSuccess(result, out, err, "Error running list")
+
+ search_filter = "(objectClass=group)"
+
+ grouplist = self.samdb.search(base=self.samdb.normalize_dn_in_domain(base_dn),
+ scope=ldb.SCOPE_SUBTREE,
+ expression=search_filter,
+ attrs=["name"])
+
+ self.assertTrue(len(grouplist) > 0, "no groups found in samdb")
+
+ for groupobj in grouplist:
+ name = str(groupobj.get("name", idx=0))
+ found = self.assertMatch(out, name,
+ "group '%s' not found" % name)
+
+ def test_listmembers(self):
+ (result, out, err) = self.runsubcmd("group", "listmembers", "Domain Users",
+ "-H", "ldap://%s" % os.environ["DC_SERVER"],
+ "-U%s%%%s" % (os.environ["DC_USERNAME"],
+ os.environ["DC_PASSWORD"]))
+ self.assertCmdSuccess(result, out, err, "Error running listmembers")
+
+ search_filter = "(|(primaryGroupID=513)(memberOf=CN=Domain Users,CN=Users,%s))" % self.samdb.domain_dn()
+
+ grouplist = self.samdb.search(base=self.samdb.domain_dn(),
+ scope=ldb.SCOPE_SUBTREE,
+ expression=search_filter,
+ attrs=["samAccountName"])
+
+ self.assertTrue(len(grouplist) > 0, "no groups found in samdb")
+
+ for groupobj in grouplist:
+ name = str(groupobj.get("samAccountName", idx=0))
+ found = self.assertMatch(out, name, "group '%s' not found" % name)
+
+ def test_listmembers_hide_expired(self):
+ expire_username = "expireUser"
+ expire_user = self._random_user({"name": expire_username})
+ self._create_user(expire_user)
+
+ (result, out, err) = self.runsubcmd(
+ "group",
+ "listmembers",
+ "Domain Users",
+ "--hide-expired",
+ "-H",
+ "ldap://%s" % os.environ["DC_SERVER"],
+ "-U%s%%%s" % (os.environ["DC_USERNAME"],
+ os.environ["DC_PASSWORD"]))
+ self.assertCmdSuccess(result, out, err, "Error running listmembers")
+ self.assertTrue(expire_username in out,
+ "user '%s' not found" % expire_username)
+
+ # user will be expired one second ago
+ self.samdb.setexpiry(
+ "(sAMAccountname=%s)" % expire_username,
+ -1,
+ False)
+
+ (result, out, err) = self.runsubcmd(
+ "group",
+ "listmembers",
+ "Domain Users",
+ "--hide-expired",
+ "-H",
+ "ldap://%s" % os.environ["DC_SERVER"],
+ "-U%s%%%s" % (os.environ["DC_USERNAME"],
+ os.environ["DC_PASSWORD"]))
+ self.assertCmdSuccess(result, out, err, "Error running listmembers")
+ self.assertFalse(expire_username in out,
+ "user '%s' not found" % expire_username)
+
+ self.samdb.deleteuser(expire_username)
+
+ def test_listmembers_hide_disabled(self):
+ disable_username = "disableUser"
+ disable_user = self._random_user({"name": disable_username})
+ self._create_user(disable_user)
+
+ (result, out, err) = self.runsubcmd(
+ "group",
+ "listmembers",
+ "Domain Users",
+ "--hide-disabled",
+ "-H",
+ "ldap://%s" % os.environ["DC_SERVER"],
+ "-U%s%%%s" % (os.environ["DC_USERNAME"],
+ os.environ["DC_PASSWORD"]))
+ self.assertCmdSuccess(result, out, err, "Error running listmembers")
+ self.assertTrue(disable_username in out,
+ "user '%s' not found" % disable_username)
+
+ self.samdb.disable_account("(sAMAccountname=%s)" % disable_username)
+
+ (result, out, err) = self.runsubcmd(
+ "group",
+ "listmembers",
+ "Domain Users",
+ "--hide-disabled",
+ "-H",
+ "ldap://%s" % os.environ["DC_SERVER"],
+ "-U%s%%%s" % (os.environ["DC_USERNAME"],
+ os.environ["DC_PASSWORD"]))
+ self.assertCmdSuccess(result, out, err, "Error running listmembers")
+ self.assertFalse(disable_username in out,
+ "user '%s' not found" % disable_username)
+
+ self.samdb.deleteuser(disable_username)
+
+ def test_listmembers_full_dn(self):
+ (result, out, err) = self.runsubcmd("group", "listmembers", "Domain Users",
+ "--full-dn",
+ "-H", "ldap://%s" % os.environ["DC_SERVER"],
+ "-U%s%%%s" % (os.environ["DC_USERNAME"],
+ os.environ["DC_PASSWORD"]))
+ self.assertCmdSuccess(result, out, err, "Error running listmembers")
+
+ search_filter = "(|(primaryGroupID=513)(memberOf=CN=Domain Users,CN=Users,%s))" % self.samdb.domain_dn()
+
+ grouplist = self.samdb.search(base=self.samdb.domain_dn(),
+ scope=ldb.SCOPE_SUBTREE,
+ expression=search_filter,
+ attrs=["dn"])
+
+ self.assertTrue(len(grouplist) > 0, "no groups found in samdb")
+
+ for groupobj in grouplist:
+ name = str(groupobj.get("dn", idx=0))
+ found = self.assertMatch(out, name, "group '%s' not found" % name)
+
+
+ def test_move(self):
+ full_ou_dn = str(self.samdb.normalize_dn_in_domain("OU=movetest_grp"))
+ self.addCleanup(self.samdb.delete, full_ou_dn, ["tree_delete:1"])
+
+ (result, out, err) = self.runsubcmd("ou", "add", full_ou_dn)
+ self.assertCmdSuccess(result, out, err)
+ self.assertEqual(err, "", "There shouldn't be any error message")
+ self.assertIn('Added ou "%s"' % full_ou_dn, out)
+
+ for group in self.groups:
+ (result, out, err) = self.runsubcmd(
+ "group", "move", group["name"], full_ou_dn)
+ self.assertCmdSuccess(result, out, err, "Error running move")
+ self.assertIn('Moved group "%s" into "%s"' %
+ (group["name"], full_ou_dn), out)
+
+ # Should fail as groups objects are in OU
+ (result, out, err) = self.runsubcmd("ou", "delete", full_ou_dn)
+ self.assertCmdFail(result)
+ self.assertIn(("subtree_delete: Unable to delete a non-leaf node "
+ "(it has %d children)!") % len(self.groups), err)
+
+ for group in self.groups:
+ new_dn = "CN=Users,%s" % self.samdb.domain_dn()
+ (result, out, err) = self.runsubcmd(
+ "group", "move", group["name"], new_dn)
+ self.assertCmdSuccess(result, out, err, "Error running move")
+ self.assertIn('Moved group "%s" into "%s"' %
+ (group["name"], new_dn), out)
+
+ def test_show(self):
+ """Assert that we can show a group correctly."""
+ (result, out, err) = self.runsubcmd("group", "show", "Domain Users",
+ "-H", "ldap://%s" % os.environ["DC_SERVER"],
+ "-U%s%%%s" % (os.environ["DC_USERNAME"],
+ os.environ["DC_PASSWORD"]))
+ self.assertCmdSuccess(result, out, err)
+ self.assertEqual(err, "", "Shouldn't be any error messages")
+ self.assertIn("dn: CN=Domain Users,CN=Users,DC=addom,DC=samba,DC=example,DC=com", out)
+
+ def test_rename_samaccountname(self):
+ """rename the samaccountname of all groups"""
+ for group in self.groups:
+ new_name = "new_samaccountname_of_" + group["name"]
+
+ # change samaccountname
+ (result, out, err) = self.runsubcmd("group", "rename", group["name"],
+ "--samaccountname=" + new_name)
+ self.assertCmdSuccess(result, out, err)
+ self.assertEqual(err, "", "Shouldn't be any error messages")
+ self.assertIn('successfully', out)
+
+ found = self._find_group(new_name)
+ self.assertEqual("%s" % found.get("description"), group["description"])
+ if not "cn" in group or str(group["cn"]) == str(group["name"]):
+ self.assertEqual("%s" % found.get("cn"), new_name)
+ else:
+ self.assertEqual("%s" % found.get("cn"), group["cn"])
+
+ # trying to remove the samaccountname throws an error
+ (result, out, err) = self.runsubcmd("group", "rename", new_name,
+ "--samaccountname=")
+ self.assertCmdFail(result)
+ self.assertIn('Failed to rename group', err)
+ self.assertIn('delete protected attribute', err)
+
+ # reset changes
+ (result, out, err) = self.runsubcmd("group", "rename", new_name,
+ "--samaccountname=" + group["name"])
+ self.assertCmdSuccess(result, out, err)
+ if "cn" in group:
+ (result, out, err) = self.runsubcmd("group", "rename", group["name"],
+ "--force-new-cn=%s" % group["cn"])
+ self.assertCmdSuccess(result, out, err)
+
+ def test_rename_cn_mail(self):
+ """change and remove the cn and mail attributes of all groups"""
+ for group in self.groups:
+ new_mail = "new mail of " + group["name"]
+ new_cn = "new cn of " + group["name"]
+
+ # change attributes
+ (result, out, err) = self.runsubcmd("group", "rename", group["name"],
+ "--mail-address=" + new_mail,
+ "--force-new-cn=" + new_cn)
+ self.assertCmdSuccess(result, out, err)
+ self.assertEqual(err, "", "Shouldn't be any error messages")
+ self.assertIn('successfully', out)
+
+ found = self._find_group(group["name"])
+ self.assertEqual("%s" % found.get("mail"), new_mail)
+ self.assertEqual("%s" % found.get("cn"), new_cn)
+
+ # remove mail
+ (result, out, err) = self.runsubcmd("group", "rename", group["name"],
+ "--mail-address=")
+ self.assertCmdSuccess(result, out, err)
+ self.assertEqual(err, "", "Shouldn't be any error messages")
+ self.assertIn('successfully', out)
+
+ found = self._find_group(group["name"])
+ self.assertEqual(found.get("mail"), None)
+
+ # trying to remove cn (throws an error)
+ (result, out, err) = self.runsubcmd("group", "rename", group["name"],
+ "--force-new-cn=")
+ self.assertCmdFail(result)
+ self.assertIn("Failed to rename group", err)
+ self.assertIn("delete protected attribute", err)
+
+ # reset CN (mail is already empty)
+ (result, out, err) = self.runsubcmd("group", "rename", group["name"],
+ "--reset-cn")
+ self.assertCmdSuccess(result, out, err)
+
+ def _randomGroup(self, base={}):
+ """create a group with random attribute values, you can specify base
+ attributes"""
+ group = {
+ "name": self.randomName(),
+ "description": self.randomName(count=100),
+ "createGroupFn": self._create_group,
+ "checkGroupFn": self._check_group,
+ }
+ group.update(base)
+ return group
+
+ def _randomPosixGroup(self, base={}):
+ """create a group with random attribute values and additional RFC2307
+ attributes, you can specify base attributes"""
+ group = self._randomGroup({})
+ group.update(base)
+ posixAttributes = {
+ "unixdomain": self.randomName(),
+ "gidNumber": self.randomXid(),
+ "createGroupFn": self._create_posix_group,
+ "checkGroupFn": self._check_posix_group,
+ }
+ group.update(posixAttributes)
+ group.update(base)
+ return group
+
+ def _randomUnixGroup(self, base={}):
+ """create a group with random attribute values and additional RFC2307
+ attributes, you can specify base attributes"""
+ group = self._randomGroup({})
+ group.update(base)
+ posixAttributes = {
+ "gidNumber": self.randomXid(),
+ "createGroupFn": self._create_unix_group,
+ "checkGroupFn": self._check_unix_group,
+ }
+ group.update(posixAttributes)
+ group.update(base)
+ return group
+
+ def _check_group(self, group):
+ """ check if a group from SamDB has the same attributes as
+ its template """
+ found = self._find_group(group["name"])
+
+ self.assertEqual("%s" % found.get("name"), group["name"])
+ self.assertEqual("%s" % found.get("description"), group["description"])
+
+ def _check_posix_group(self, group):
+ """ check if a posix_group from SamDB has the same attributes as
+ its template """
+ found = self._find_group(group["name"])
+
+ self.assertEqual("%s" % found.get("gidNumber"), "%s" %
+ group["gidNumber"])
+ self._check_group(group)
+
+ def _check_unix_group(self, group):
+ """ check if a unix_group from SamDB has the same attributes as its
+template """
+ found = self._find_group(group["name"])
+
+ self.assertEqual("%s" % found.get("gidNumber"), "%s" %
+ group["gidNumber"])
+ self._check_group(group)
+
+ def _create_group(self, group):
+ return self.runsubcmd("group", "add", group["name"],
+ "--description=%s" % group["description"],
+ "-H", "ldap://%s" % os.environ["DC_SERVER"],
+ "-U%s%%%s" % (os.environ["DC_USERNAME"],
+ os.environ["DC_PASSWORD"]))
+
+ def _create_posix_group(self, group):
+ """ create a new group with RFC2307 attributes """
+ return self.runsubcmd("group", "add", group["name"],
+ "--description=%s" % group["description"],
+ "--nis-domain=%s" % group["unixdomain"],
+ "--gid-number=%s" % group["gidNumber"],
+ "-H", "ldap://%s" % os.environ["DC_SERVER"],
+ "-U%s%%%s" % (os.environ["DC_USERNAME"],
+ os.environ["DC_PASSWORD"]))
+
+ def _create_unix_group(self, group):
+ """ Add RFC2307 attributes to a group"""
+ self._create_group(group)
+ return self.runsubcmd("group", "addunixattrs", group["name"],
+ "%s" % group["gidNumber"],
+ "-H", "ldap://%s" % os.environ["DC_SERVER"],
+ "-U%s%%%s" % (os.environ["DC_USERNAME"],
+ os.environ["DC_PASSWORD"]))
+
+ def _find_group(self, name):
+ search_filter = ("(&(sAMAccountName=%s)(objectCategory=%s,%s))" %
+ (ldb.binary_encode(name),
+ "CN=Group,CN=Schema,CN=Configuration",
+ self.samdb.domain_dn()))
+ grouplist = self.samdb.search(base=self.samdb.domain_dn(),
+ scope=ldb.SCOPE_SUBTREE,
+ expression=search_filter)
+ if grouplist:
+ return grouplist[0]
+ else:
+ return None
+
+ def test_stats(self):
+ (result, out, err) = self.runsubcmd("group", "stats",
+ "-H", "ldap://%s" % os.environ["DC_SERVER"],
+ "-U%s%%%s" % (os.environ["DC_USERNAME"],
+ os.environ["DC_PASSWORD"]))
+ self.assertCmdSuccess(result, out, err, "Error running stats")
+
+ # sanity-check the command reports 'total groups' correctly
+ search_filter = "(objectClass=group)"
+ grouplist = self.samdb.search(base=self.samdb.domain_dn(),
+ scope=ldb.SCOPE_SUBTREE,
+ expression=search_filter,
+ attrs=[])
+
+ total_groups = len(grouplist)
+ self.assertTrue("Total groups: {0}".format(total_groups) in out,
+ "Total groups not reported correctly")
+
+ def _random_user(self, base=None):
+ """
+ create a user with random attribute values, you can specify
+ base attributes
+ """
+ if base is None:
+ base = {}
+ user = {
+ "name": self.randomName(),
+ "password": self.random_password(16),
+ "surname": self.randomName(),
+ "given-name": self.randomName(),
+ "job-title": self.randomName(),
+ "department": self.randomName(),
+ "company": self.randomName(),
+ "description": self.randomName(count=100),
+ "createUserFn": self._create_user,
+ }
+ user.update(base)
+ return user
+
+ def _create_user(self, user):
+ return self.runsubcmd(
+ "user",
+ "add",
+ user["name"],
+ user["password"],
+ "--surname=%s" % user["surname"],
+ "--given-name=%s" % user["given-name"],
+ "--job-title=%s" % user["job-title"],
+ "--department=%s" % user["department"],
+ "--description=%s" % user["description"],
+ "--company=%s" % user["company"],
+ "-H",
+ "ldap://%s" % os.environ["DC_SERVER"],
+ "-U%s%%%s" % (os.environ["DC_USERNAME"],
+ os.environ["DC_PASSWORD"]))
diff --git a/python/samba/tests/samba_tool/group_edit.sh b/python/samba/tests/samba_tool/group_edit.sh
new file mode 100755
index 0000000..3db2c66
--- /dev/null
+++ b/python/samba/tests/samba_tool/group_edit.sh
@@ -0,0 +1,228 @@
+#!/bin/sh
+#
+# Test for 'samba-tool group edit'
+
+if [ $# -lt 3 ]; then
+ cat <<EOF
+Usage: group_edit.sh SERVER USERNAME PASSWORD
+EOF
+ exit 1
+fi
+
+SERVER="$1"
+USERNAME="$2"
+PASSWORD="$3"
+
+samba_ldbsearch=ldbsearch
+if test -x $BINDIR/ldbsearch; then
+ samba_ldbsearch=$BINDIR/ldbsearch
+fi
+
+STpath=$(pwd)
+. $STpath/testprogs/blackbox/subunit.sh
+
+display_name="Users in Göttingen"
+display_name_b64="VXNlcnMgaW4gR8O2dHRpbmdlbg=="
+display_name_new="Users in Goettingen"
+# attribute value including control character
+# echo -e "test \a string" | base64
+display_name_con_b64="dGVzdCAHIHN0cmluZwo="
+
+tmpeditor=$(mktemp --suffix .sh -p $SELFTEST_TMPDIR samba-tool-editor-XXXXXXXX)
+chmod +x $tmpeditor
+
+create_test_group()
+{
+ $PYTHON ${STpath}/source4/scripting/bin/samba-tool \
+ group add testgroup1 \
+ -H "ldap://$SERVER" "-U$USERNAME" "--password=$PASSWORD"
+}
+
+delete_test_group()
+{
+ $PYTHON ${STpath}/source4/scripting/bin/samba-tool \
+ group delete testgroup1 \
+ -H "ldap://$SERVER" "-U$USERNAME" "--password=$PASSWORD"
+}
+
+create_test_user()
+{
+ $PYTHON ${STpath}/source4/scripting/bin/samba-tool \
+ user create testuser1 --random-password \
+ -H "ldap://$SERVER" "-U$USERNAME" "--password=$PASSWORD"
+}
+
+delete_test_user()
+{
+ $PYTHON ${STpath}/source4/scripting/bin/samba-tool \
+ user delete testuser1 \
+ -H "ldap://$SERVER" "-U$USERNAME" "--password=$PASSWORD"
+}
+
+add_member()
+{
+ user_dn=$($PYTHON ${STpath}/source4/scripting/bin/samba-tool \
+ user show testuser1 --attributes=dn \
+ -H "ldap://$SERVER" "-U$USERNAME" "--password=$PASSWORD" |
+ grep ^dn: | cut -d' ' -f2)
+
+ # create editor.sh
+ cat >$tmpeditor <<EOF
+#!/usr/bin/env bash
+group_ldif="\$1"
+
+grep -v '^$' \$group_ldif > \${group_ldif}.tmp
+echo "member: $user_dn" >> \${group_ldif}.tmp
+
+mv \${group_ldif}.tmp \$group_ldif
+EOF
+
+ $PYTHON ${STpath}/source4/scripting/bin/samba-tool \
+ group edit testgroup1 --editor=$tmpeditor \
+ -H "ldap://$SERVER" "-U$USERNAME" "--password=$PASSWORD"
+}
+
+get_member()
+{
+ $PYTHON ${STpath}/source4/scripting/bin/samba-tool \
+ group listmembers testgroup1 \
+ -H "ldap://$SERVER" "-U$USERNAME" "--password=$PASSWORD"
+}
+
+# Test edit group - add base64 attributes
+add_attribute_base64()
+{
+ # create editor.sh
+ cat >$tmpeditor <<EOF
+#!/usr/bin/env bash
+group_ldif="\$1"
+
+grep -v '^$' \$group_ldif > \${group_ldif}.tmp
+echo "displayName:: $display_name_b64" >> \${group_ldif}.tmp
+
+mv \${group_ldif}.tmp \$group_ldif
+EOF
+
+ $PYTHON ${STpath}/source4/scripting/bin/samba-tool group edit \
+ testgroup1 --editor=$tmpeditor \
+ -H "ldap://$SERVER" "-U$USERNAME" "--password=$PASSWORD"
+}
+
+get_attribute_base64()
+{
+ $samba_ldbsearch '(sAMAccountName=testgroup1)' displayName \
+ -H "ldap://$SERVER" "-U$USERNAME" "--password=$PASSWORD"
+}
+
+delete_attribute()
+{
+ # create editor.sh
+ cat >$tmpeditor <<EOF
+#!/usr/bin/env bash
+group_ldif="\$1"
+
+grep -v '^displayName' \$group_ldif >> \${group_ldif}.tmp
+mv \${group_ldif}.tmp \$group_ldif
+EOF
+ $PYTHON ${STpath}/source4/scripting/bin/samba-tool group edit \
+ testgroup1 --editor=$tmpeditor \
+ -H "ldap://$SERVER" "-U$USERNAME" "--password=$PASSWORD"
+}
+
+# Test edit group - add base64 attribute value including control character
+add_attribute_base64_control()
+{
+ # create editor.sh
+ cat >$tmpeditor <<EOF
+#!/usr/bin/env bash
+group_ldif="\$1"
+
+grep -v '^$' \$group_ldif > \${group_ldif}.tmp
+echo "displayName:: $display_name_con_b64" >> \${group_ldif}.tmp
+
+mv \${group_ldif}.tmp \$group_ldif
+EOF
+ $PYTHON ${STpath}/source4/scripting/bin/samba-tool group edit \
+ testgroup1 --editor=$tmpeditor \
+ -H "ldap://$SERVER" "-U$USERNAME" "--password=$PASSWORD"
+}
+
+get_attribute_base64_control()
+{
+ $PYTHON ${STpath}/source4/scripting/bin/samba-tool group show \
+ testgroup1 --attributes=displayName \
+ -H "ldap://$SERVER" "-U$USERNAME" "--password=$PASSWORD"
+}
+
+get_attribute_force_no_base64()
+{
+ # LDB_FLAG_FORCE_NO_BASE64_LDIF should be used here.
+ $PYTHON ${STpath}/source4/scripting/bin/samba-tool group show \
+ testgroup1 --attributes=displayName \
+ -H "ldap://$SERVER" "-U$USERNAME" "--password=$PASSWORD"
+}
+
+# Test edit group - change base64 attribute value including control character
+change_attribute_base64_control()
+{
+ # create editor.sh
+ cat >$tmpeditor <<EOF
+#!/usr/bin/env bash
+group_ldif="\$1"
+
+sed -i -e 's/displayName:: $display_name_con_b64/displayName: $display_name/' \
+ \$group_ldif
+EOF
+ $PYTHON ${STpath}/source4/scripting/bin/samba-tool group edit \
+ testgroup1 --editor=$tmpeditor \
+ -H "ldap://$SERVER" "-U$USERNAME" "--password=$PASSWORD"
+}
+
+# Test edit group - change attributes with LDB_FLAG_FORCE_NO_BASE64_LDIF
+change_attribute_force_no_base64()
+{
+ # create editor.sh
+ # Expects that the original attribute is available as clear text,
+ # because the LDB_FLAG_FORCE_NO_BASE64_LDIF should be used here.
+ cat >$tmpeditor <<EOF
+#!/usr/bin/env bash
+group_ldif="\$1"
+
+sed -i -e 's/displayName: $display_name/displayName: $display_name_new/' \
+ \$group_ldif
+EOF
+
+ $PYTHON ${STpath}/source4/scripting/bin/samba-tool group edit \
+ testgroup1 --editor=$tmpeditor \
+ -H "ldap://$SERVER" "-U$USERNAME" "--password=$PASSWORD"
+}
+
+get_changed_attribute_force_no_base64()
+{
+ $PYTHON ${STpath}/source4/scripting/bin/samba-tool group show \
+ testgroup1 --attributes=displayName \
+ -H "ldap://$SERVER" "-U$USERNAME" "--password=$PASSWORD"
+}
+
+failed=0
+
+testit "create_test_group" create_test_group || failed=$(expr $failed + 1)
+testit "create_test_user" create_test_user || failed=$(expr $failed + 1)
+testit "add_member" add_member || failed=$(expr $failed + 1)
+testit_grep "get_member" "^testuser1" get_member || failed=$(expr $failed + 1)
+testit "add_attribute_base64" add_attribute_base64 || failed=$(expr $failed + 1)
+testit_grep "get_attribute_base64" "^displayName:: $display_name_b64" get_attribute_base64 || failed=$(expr $failed + 1)
+testit "delete_attribute" delete_attribute || failed=$(expr $failed + 1)
+testit "add_attribute_base64_control" add_attribute_base64_control || failed=$(expr $failed + 1)
+testit_grep "get_attribute_base64_control" "^displayName:: $display_name_con_b64" get_attribute_base64_control || failed=$(expr $failed + 1)
+testit "change_attribute_base64_control" change_attribute_base64_control || failed=$(expr $failed + 1)
+testit_grep "get_attribute_base64" "^displayName:: $display_name_b64" get_attribute_base64 || failed=$(expr $failed + 1)
+testit_grep "get_attribute_force_no_base64" "^displayName: $display_name" get_attribute_force_no_base64 || failed=$(expr $failed + 1)
+testit "change_attribute_force_no_base64" change_attribute_force_no_base64 || failed=$(expr $failed + 1)
+testit_grep "get_changed_attribute_force_no_base64" "^displayName: $display_name_new" get_changed_attribute_force_no_base64 || failed=$(expr $failed + 1)
+testit "delete_test_group" delete_test_group || failed=$(expr $failed + 1)
+testit "delete_test_user" delete_test_user || failed=$(expr $failed + 1)
+
+rm -f $tmpeditor
+
+exit $failed
diff --git a/python/samba/tests/samba_tool/help.py b/python/samba/tests/samba_tool/help.py
new file mode 100644
index 0000000..fa7836d
--- /dev/null
+++ b/python/samba/tests/samba_tool/help.py
@@ -0,0 +1,81 @@
+# Unix SMB/CIFS implementation.
+# Copyright (C) Catalyst IT Ltd 2017.
+#
+# Originally written by Douglas Bagnall <douglas.bagnall@catalyst.net.nz>
+#
+# 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 re
+from samba.tests.samba_tool.base import SambaToolCmdTest
+from samba.tests import BlackboxProcessError
+from samba.tests import check_help_consistency
+from samba.common import get_string
+
+
+class HelpTestCase(SambaToolCmdTest):
+ """Tests for samba-tool help and --help
+
+ We test for consistency and lack of crashes."""
+
+ def _find_sub_commands(self, args):
+ self.runcmd(*args)
+
+ def test_help_tree(self):
+ # we call actual subprocesses, because we are probing the
+ # actual help output where there is no sub-command. Don't copy
+ # this if you have an actual command: for that use
+ # self.runcmd() or self.runsubcmd().
+ known_commands = [[]]
+ failed_commands = []
+
+ for i in range(4):
+ new_commands = []
+ for c in known_commands:
+ line = ' '.join(['samba-tool'] + c + ['--help'])
+ try:
+ output = self.check_output(line)
+ except BlackboxProcessError as e:
+ output = e.stdout
+ failed_commands.append(c)
+ output = get_string(output)
+ tail = output.partition('Available subcommands:')[2]
+ subcommands = re.findall(r'^\s*([\w-]+)\s+-', tail,
+ re.MULTILINE)
+ for s in subcommands:
+ new_commands.append(c + [s])
+
+ # check that `samba-tool help X Y` == `samba-tool X Y --help`
+ line = ' '.join(['samba-tool', 'help'] + c)
+ try:
+ output2 = self.check_output(line)
+ except BlackboxProcessError as e:
+ output2 = e.stdout
+ failed_commands.append(c)
+
+ output2 = get_string(output2)
+ self.assertEqual(output, output2)
+
+ err = check_help_consistency(output,
+ options_start='Options:',
+ options_end='Available subcommands:')
+ if err is not None:
+ self.fail("consistency error with %s:\n%s" % (line, err))
+
+ if not new_commands:
+ break
+
+ known_commands = new_commands
+
+ self.assertEqual(failed_commands, [])
diff --git a/python/samba/tests/samba_tool/join.py b/python/samba/tests/samba_tool/join.py
new file mode 100644
index 0000000..0cbd319
--- /dev/null
+++ b/python/samba/tests/samba_tool/join.py
@@ -0,0 +1,31 @@
+# Unix SMB/CIFS implementation.
+# Copyright (C) Andrew Bartlett <abartlet@samba.org> 2016
+#
+# 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 os
+import ldb
+from samba.tests.samba_tool.base import SambaToolCmdTest
+
+
+class JoinCmdTestCase(SambaToolCmdTest):
+ """Test for samba-tool domain join subcommand"""
+
+ def test_rejoin(self):
+ """Run domain join to confirm it errors because we are already joined"""
+ (result, out, err) = self.runsubcmd("domain", "join", os.environ["REALM"], "dc", "-U%s%%%s" % (os.environ["USERNAME"], os.environ["PASSWORD"]))
+
+ self.assertCmdFail(result)
+ self.assertTrue("Not removing account" in err, "Should fail with exception")
diff --git a/python/samba/tests/samba_tool/join_lmdb_size.py b/python/samba/tests/samba_tool/join_lmdb_size.py
new file mode 100644
index 0000000..7b43c45
--- /dev/null
+++ b/python/samba/tests/samba_tool/join_lmdb_size.py
@@ -0,0 +1,152 @@
+# Unix SMB/CIFS implementation.
+# Copyright (C) Catalyst IT Ltd. 2019
+#
+# 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.tests.samba_tool.base import SambaToolCmdTest
+import os
+import shutil
+
+
+class JoinLmdbSizeTestCase(SambaToolCmdTest):
+ """Test setting of the lmdb map size during join"""
+
+ def setUp(self):
+ super().setUp()
+ self.tempsambadir = os.path.join(self.tempdir, "samba")
+ os.mkdir(self.tempsambadir)
+ (_, name) = os.path.split(self.tempdir)
+ self.netbios_name = name
+
+ # join a domain and set the lmdb map size to size
+ #
+ # returns the tuple (ret, stdout, stderr)
+ def join(self, size=None, role=None):
+ command = (
+ "samba-tool " +
+ "domain join " +
+ os.environ["REALM"] + " " +
+ role + " " +
+ ("-U%s%%%s " % (os.environ["USERNAME"], os.environ["PASSWORD"])) +
+ ("--targetdir=%s " % self.tempsambadir) +
+ ("--option=netbiosname=%s " % self.netbios_name) +
+ "--backend-store=mdb "
+ )
+ if size:
+ command += ("--backend-store-size=%s" % size)
+
+ (ret, stdout, stderr) = self.run_command(command)
+ if ret == 0:
+ self.cleanup_join(self.netbios_name)
+
+ return (ret, stdout, stderr)
+
+ def is_rodc(self):
+ url = "ldb://%s/private/sam.ldb" % self.tempsambadir
+ samdb = self.getSamDB("-H", url)
+ return samdb.am_rodc()
+
+ #
+ # Get the lmdb map size for the specified command
+ #
+ # While there is a python lmdb package available we use the lmdb command
+ # line utilities to avoid introducing a dependency.
+ #
+ def get_lmdb_environment_size(self, path):
+ (result, out, err) = self.run_command("mdb_stat -ne %s" % path)
+ if result:
+ self.fail("Unable to run mdb_stat\n")
+ for line in out.split("\n"):
+ line = line.strip()
+ if line.startswith("Map size:"):
+ line = line.replace(" ", "")
+ (label, size) = line.split(":")
+ return int(size)
+
+ #
+ # Check the lmdb files created by join and ensure that the map size
+ # has been set to size.
+ #
+ # Currently this is all the *.ldb files in private/sam.ldb.d
+ #
+ def check_lmdb_environment_sizes(self, size):
+ directory = os.path.join(self.tempsambadir, "private", "sam.ldb.d")
+ for name in os.listdir(directory):
+ if name.endswith(".ldb"):
+ path = os.path.join(directory, name)
+ s = self.get_lmdb_environment_size(path)
+ if s != size:
+ self.fail("File %s, size=%d larger than %d" %
+ (name, s, size))
+
+ #
+ # Ensure that if --backend-store-size is not specified the default of
+ # 8Gb is used
+ def test_join_as_dc_default(self):
+ (result, out, err) = self.join(role="DC")
+ self.assertEqual(0, result)
+ self.check_lmdb_environment_sizes(8 * 1024 * 1024 * 1024)
+ self.assertFalse(self.is_rodc())
+
+ #
+ # Join as an DC with the lmdb backend size set to 1Gb
+ def test_join_as_dc(self):
+ (result, out, err) = self.join("1Gb", "DC")
+ self.assertEqual(0, result)
+ self.check_lmdb_environment_sizes(1 * 1024 * 1024 * 1024)
+ self.assertFalse(self.is_rodc())
+
+ #
+ # Join as an RODC with the lmdb backend size set to 128Mb
+ def test_join_as_rodc(self):
+ (result, out, err) = self.join("128Mb", "RODC")
+ self.assertEqual(0, result)
+ self.check_lmdb_environment_sizes(128 * 1024 * 1024)
+ self.assertTrue(self.is_rodc())
+
+ #
+ # Join as an RODC with --backend-store-size
+ def test_join_as_rodc_default(self):
+ (result, out, err) = self.join(role="RODC")
+ self.assertEqual(0, result)
+ self.check_lmdb_environment_sizes(8 * 1024 * 1024 * 1024)
+ self.assertTrue(self.is_rodc())
+
+ def test_no_unit_suffix(self):
+ (result, out, err) = self.run_command(
+ 'samba-tool domain join --backend-store-size "2"')
+ self.assertGreater(result, 0)
+ self.assertRegex(err,
+ r"--backend-store-size invalid suffix ''")
+
+ def test_invalid_unit_suffix(self):
+ (result, out, err) = self.run_command(
+ 'samba-tool domain join --backend-store-size "2 cd"')
+ self.assertGreater(result, 0)
+ self.assertRegex(err,
+ r"--backend-store-size invalid suffix 'cd'")
+
+ def test_non_numeric(self):
+ (result, out, err) = self.run_command(
+ 'samba-tool domain join --backend-store-size "two Gb"')
+ self.assertGreater(result, 0)
+ self.assertRegex(
+ err,
+ r"backend-store-size option requires a numeric value, with an"
+ " optional unit suffix")
+
+ def tearDown(self):
+ super().tearDown()
+ shutil.rmtree(self.tempsambadir)
diff --git a/python/samba/tests/samba_tool/join_member.py b/python/samba/tests/samba_tool/join_member.py
new file mode 100644
index 0000000..c2ab02f
--- /dev/null
+++ b/python/samba/tests/samba_tool/join_member.py
@@ -0,0 +1,71 @@
+# Unix SMB/CIFS implementation.
+# Copyright (C) David Mulder <dmulder@samba.org> 2021
+#
+# 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 os
+import re
+from samba.tests.samba_tool.base import SambaToolCmdTest
+from samba.param import LoadParm
+from samba.netcmd.common import netcmd_dnsname
+
+class JoinMemberCmdTestCase(SambaToolCmdTest):
+ """Test for samba-tool domain join subcommand"""
+
+ def test_join_member(self):
+ """Run a domain member join, and check that dns is updated"""
+ smb_conf = os.environ["SERVERCONFFILE"]
+ zone = os.environ["REALM"].lower()
+ lp = LoadParm()
+ lp.load(smb_conf)
+ dnsname = netcmd_dnsname(lp)
+ # Fetch the existing dns A records
+ (result, out, err) = self.runsubcmd("dns", "query",
+ os.environ["DC_SERVER"],
+ zone, dnsname, 'A',
+ "-s", smb_conf,
+ "-U%s%%%s" % (os.environ["DC_USERNAME"],
+ os.environ["DC_PASSWORD"]))
+ self.assertCmdSuccess(result, out, err, 'Failed to find the record')
+
+ existing_records = re.findall('A:\s+(\d+\.\d+\.\d+\.\d+)\s', out)
+
+ # Remove the existing records
+ for record in existing_records:
+ (result, out, err) = self.runsubcmd("dns", "delete",
+ os.environ["DC_SERVER"],
+ zone, dnsname, 'A', record,
+ "-s", smb_conf,
+ "-U%s%%%s" % (os.environ["DC_USERNAME"],
+ os.environ["DC_PASSWORD"]))
+ self.assertCmdSuccess(result, out, err, 'Failed to remove record')
+
+ # Perform the s3 member join (net ads join)
+ (result, out, err) = self.runsubcmd("domain", "join",
+ os.environ["REALM"], "member",
+ "-s", smb_conf,
+ "-U%s%%%s" % (os.environ["DC_USERNAME"],
+ os.environ["DC_PASSWORD"]))
+ self.assertCmdSuccess(result, out, err, 'Failed to join member')
+
+ # Ensure the dns A record was created
+ (result, out, err) = self.runsubcmd("dns", "query",
+ os.environ["DC_SERVER"],
+ zone, dnsname, 'A',
+ "-s", smb_conf,
+ "-U%s%%%s" % (os.environ["DC_USERNAME"],
+ os.environ["DC_PASSWORD"]))
+ self.assertCmdSuccess(result, out, err,
+ 'Failed to find dns host records for %s' % dnsname)
diff --git a/python/samba/tests/samba_tool/ntacl.py b/python/samba/tests/samba_tool/ntacl.py
new file mode 100644
index 0000000..1173101
--- /dev/null
+++ b/python/samba/tests/samba_tool/ntacl.py
@@ -0,0 +1,247 @@
+# Unix SMB/CIFS implementation.
+# Copyright (C) Andrew Bartlett 2012
+#
+# Based on user.py:
+# Copyright (C) Sean Dague <sdague@linux.vnet.ibm.com> 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/>.
+#
+
+import os
+from samba.tests.samba_tool.base import SambaToolCmdTest
+from samba.tests import env_loadparm
+import random
+
+
+class NtACLCmdSysvolTestCase(SambaToolCmdTest):
+ """Tests for samba-tool ntacl sysvol* subcommands"""
+
+ def test_ntvfs(self):
+ (result, out, err) = self.runsubcmd("ntacl", "sysvolreset",
+ "--use-ntvfs")
+ self.assertCmdSuccess(result, out, err)
+ self.assertEqual(out, "", "Shouldn't be any output messages")
+ self.assertIn("Please note that POSIX permissions have NOT been changed, only the stored NT ACL", err)
+
+ def test_s3fs(self):
+ (result, out, err) = self.runsubcmd("ntacl", "sysvolreset",
+ "--use-s3fs")
+
+ self.assertCmdSuccess(result, out, err)
+ self.assertEqual(err, "", "Shouldn't be any error messages")
+ self.assertEqual(out, "", "Shouldn't be any output messages")
+
+ def test_ntvfs_check(self):
+ (result, out, err) = self.runsubcmd("ntacl", "sysvolreset",
+ "--use-ntvfs")
+ self.assertCmdSuccess(result, out, err)
+ self.assertEqual(out, "", "Shouldn't be any output messages")
+ self.assertIn("Please note that POSIX permissions have NOT been changed, only the stored NT ACL", err)
+ # Now check they were set correctly
+ (result, out, err) = self.runsubcmd("ntacl", "sysvolcheck")
+ self.assertCmdSuccess(result, out, err)
+ self.assertEqual(err, "", "Shouldn't be any error messages")
+ self.assertEqual(out, "", "Shouldn't be any output messages")
+
+ def test_s3fs_check(self):
+ (result, out, err) = self.runsubcmd("ntacl", "sysvolreset",
+ "--use-s3fs")
+
+ self.assertCmdSuccess(result, out, err)
+ self.assertEqual(err, "", "Shouldn't be any error messages")
+ self.assertEqual(out, "", "Shouldn't be any output messages")
+
+ # Now check they were set correctly
+ (result, out, err) = self.runsubcmd("ntacl", "sysvolcheck")
+ self.assertCmdSuccess(result, out, err)
+ self.assertEqual(err, "", "Shouldn't be any error messages")
+ self.assertEqual(out, "", "Shouldn't be any output messages")
+
+ def test_with_missing_files(self):
+ lp = env_loadparm()
+ sysvol = lp.get('path', 'sysvol')
+ realm = lp.get('realm').lower()
+
+ src = os.path.join(sysvol, realm, 'Policies')
+ dest = os.path.join(sysvol, realm, 'Policies-NOT-IN-THE-EXPECTED-PLACE')
+ try:
+ os.rename(src, dest)
+
+ for args in (["sysvolreset", "--use-s3fs"],
+ ["sysvolreset", "--use-ntvfs"],
+ ["sysvolreset"],
+ ["sysvolcheck"]
+ ):
+
+ (result, out, err) = self.runsubcmd("ntacl", *args)
+ self.assertCmdFail(result, f"succeeded with {args} with missing dir")
+ self.assertNotIn("uncaught exception", err,
+ "Shouldn't be uncaught exception")
+ self.assertNotRegex(err, r'^\s*File [^,]+, line \d+, in',
+ "Shouldn't be lines of traceback")
+ self.assertEqual(out, "", "Shouldn't be any output messages")
+ finally:
+ os.rename(dest, src)
+
+
+class NtACLCmdGetSetTestCase(SambaToolCmdTest):
+ """Tests for samba-tool ntacl get/set subcommands"""
+
+ acl = "O:DAG:DUD:P(A;OICI;FA;;;DA)(A;OICI;FA;;;EA)(A;OICIIO;FA;;;CO)(A;OICI;FA;;;DA)(A;OICI;FA;;;SY)(A;OICI;0x1200a9;;;AU)(A;OICI;0x1200a9;;;ED)S:AI(OU;CIIDSA;WP;f30e3bbe-9ff0-11d1-b603-0000f80367c1;bf967aa5-0de6-11d0-a285-00aa003049e2;WD)(OU;CIIDSA;WP;f30e3bbf-9ff0-11d1-b603-0000f80367c1;bf967aa5-0de6-11d0-a285-00aa003049e2;WD)"
+
+ def test_ntvfs(self):
+ path = os.environ['SELFTEST_PREFIX']
+ tempf = os.path.join(path, "pytests" + str(int(100000 * random.random())))
+ open(tempf, 'w').write("empty")
+
+ (result, out, err) = self.runsubcmd("ntacl", "set", self.acl, tempf,
+ "--use-ntvfs")
+ self.assertCmdSuccess(result, out, err)
+ self.assertEqual(out, "", "Shouldn't be any output messages")
+ self.assertIn("Please note that POSIX permissions have NOT been changed, only the stored NT ACL", err)
+
+ def test_s3fs(self):
+ path = os.environ['SELFTEST_PREFIX']
+ tempf = os.path.join(path, "pytests" + str(int(100000 * random.random())))
+ open(tempf, 'w').write("empty")
+
+ (result, out, err) = self.runsubcmd("ntacl", "set", self.acl, tempf,
+ "--use-s3fs")
+
+ self.assertCmdSuccess(result, out, err)
+ self.assertEqual(err, "", "Shouldn't be any error messages")
+ self.assertEqual(out, "", "Shouldn't be any output messages")
+
+ def test_ntvfs_check(self):
+ path = os.environ['SELFTEST_PREFIX']
+ tempf = os.path.join(path, "pytests" + str(int(100000 * random.random())))
+ open(tempf, 'w').write("empty")
+
+ (result, out, err) = self.runsubcmd("ntacl", "set", self.acl, tempf,
+ "--use-ntvfs")
+ self.assertCmdSuccess(result, out, err)
+ self.assertEqual(out, "", "Shouldn't be any output messages")
+ self.assertIn("Please note that POSIX permissions have NOT been changed, only the stored NT ACL", err)
+
+ # Now check they were set correctly
+ (result, out, err) = self.runsubcmd("ntacl", "get", tempf,
+ "--use-ntvfs", "--as-sddl")
+ self.assertCmdSuccess(result, out, err)
+ self.assertEqual(err, "", "Shouldn't be any error messages")
+ self.assertEqual(self.acl + "\n", out, "Output should be the ACL")
+
+ def test_s3fs_check(self):
+ path = os.environ['SELFTEST_PREFIX']
+ tempf = os.path.join(path, "pytests" + str(int(100000 * random.random())))
+ open(tempf, 'w').write("empty")
+
+ (result, out, err) = self.runsubcmd("ntacl", "set", self.acl, tempf,
+ "--use-s3fs")
+ self.assertCmdSuccess(result, out, err)
+ self.assertEqual(out, "", "Shouldn't be any output messages")
+ self.assertEqual(err, "", "Shouldn't be any error messages")
+
+ # Now check they were set correctly
+ (result, out, err) = self.runsubcmd("ntacl", "get", tempf,
+ "--use-s3fs", "--as-sddl")
+ self.assertCmdSuccess(result, out, err)
+ self.assertEqual(err, "", "Shouldn't be any error messages")
+ self.assertEqual(self.acl + "\n", out, "Output should be the ACL")
+
+class NtACLCmdChangedomsidTestCase(SambaToolCmdTest):
+ """Tests for samba-tool ntacl changedomsid subcommand"""
+ maxDiff = 10000
+ acl = "O:DAG:DUD:P(A;OICI;0x001f01ff;;;DA)(A;OICI;0x001f01ff;;;EA)(A;OICIIO;0x001f01ff;;;CO)(A;OICI;0x001f01ff;;;DA)(A;OICI;0x001f01ff;;;SY)(A;OICI;0x001200a9;;;AU)(A;OICI;0x001200a9;;;ED)S:AI(OU;CIIDSA;WP;f30e3bbe-9ff0-11d1-b603-0000f80367c1;bf967aa5-0de6-11d0-a285-00aa003049e2;WD)(OU;CIIDSA;WP;f30e3bbf-9ff0-11d1-b603-0000f80367c1;bf967aa5-0de6-11d0-a285-00aa003049e2;WD)"
+ new_acl="O:S-1-5-21-2212615479-2695158682-2101375468-512G:S-1-5-21-2212615479-2695158682-2101375468-513D:P(A;OICI;FA;;;S-1-5-21-2212615479-2695158682-2101375468-512)(A;OICI;FA;;;S-1-5-21-2212615479-2695158682-2101375468-519)(A;OICIIO;FA;;;CO)(A;OICI;FA;;;S-1-5-21-2212615479-2695158682-2101375468-512)(A;OICI;FA;;;SY)(A;OICI;0x1200a9;;;AU)(A;OICI;0x1200a9;;;ED)S:AI(OU;CIIDSA;WP;f30e3bbe-9ff0-11d1-b603-0000f80367c1;bf967aa5-0de6-11d0-a285-00aa003049e2;WD)(OU;CIIDSA;WP;f30e3bbf-9ff0-11d1-b603-0000f80367c1;bf967aa5-0de6-11d0-a285-00aa003049e2;WD)"
+ domain_sid=os.environ['DOMSID']
+ new_domain_sid="S-1-5-21-2212615479-2695158682-2101375468"
+
+ def test_ntvfs_check(self):
+ path = os.environ['SELFTEST_PREFIX']
+ tempf = os.path.join(
+ path, "pytests" + str(int(100000 * random.random())))
+ open(tempf, 'w').write("empty")
+
+ print("DOMSID: %s", self.domain_sid)
+
+ (result, out, err) = self.runsubcmd("ntacl",
+ "set",
+ self.acl,
+ tempf,
+ "--use-ntvfs")
+ self.assertCmdSuccess(result, out, err)
+ self.assertEqual(out, "", "Shouldn't be any output messages")
+ self.assertIn("Please note that POSIX permissions have NOT been "
+ "changed, only the stored NT ACL", err)
+
+ (result, out, err) = self.runsubcmd("ntacl",
+ "changedomsid",
+ self.domain_sid,
+ self.new_domain_sid,
+ tempf,
+ "--use-ntvfs")
+ self.assertCmdSuccess(result, out, err)
+ self.assertEqual(out, "", "Shouldn't be any output messages")
+ self.assertIn("Please note that POSIX permissions have NOT been "
+ "changed, only the stored NT ACL.", err)
+
+ # Now check they were set correctly
+ (result, out, err) = self.runsubcmd("ntacl",
+ "get",
+ tempf,
+ "--use-ntvfs",
+ "--as-sddl")
+ self.assertCmdSuccess(result, out, err)
+ self.assertEqual(err, "", "Shouldn't be any error messages")
+ self.assertEqual(self.new_acl + "\n", out, "Output should be the ACL")
+
+ def test_s3fs_check(self):
+ path = os.environ['SELFTEST_PREFIX']
+ tempf = os.path.join(
+ path, "pytests" + str(int(100000 * random.random())))
+ open(tempf, 'w').write("empty")
+
+ print("DOMSID: %s" % self.domain_sid)
+
+ (result, out, err) = self.runsubcmd("ntacl",
+ "set",
+ self.acl,
+ tempf,
+ "--use-s3fs",
+ "--service=sysvol")
+ self.assertCmdSuccess(result, out, err)
+ self.assertEqual(out, "", "Shouldn't be any output messages")
+ self.assertEqual(err, "", "Shouldn't be any error messages")
+
+ (result, out, err) = self.runsubcmd("ntacl",
+ "changedomsid",
+ self.domain_sid,
+ self.new_domain_sid,
+ tempf,
+ "--use-s3fs",
+ "--service=sysvol")
+ self.assertCmdSuccess(result, out, err)
+ self.assertEqual(out, "", "Shouldn't be any output messages")
+ self.assertEqual(err, "", "Shouldn't be any error messages")
+
+ # Now check they were set correctly
+ (result, out, err) = self.runsubcmd("ntacl",
+ "get",
+ tempf,
+ "--use-s3fs",
+ "--as-sddl",
+ "--service=sysvol")
+ self.assertCmdSuccess(result, out, err)
+ self.assertEqual(err, "", "Shouldn't be any error messages")
+ self.assertEqual(self.new_acl + "\n", out, "Output should be the ACL")
diff --git a/python/samba/tests/samba_tool/ou.py b/python/samba/tests/samba_tool/ou.py
new file mode 100644
index 0000000..7a84876
--- /dev/null
+++ b/python/samba/tests/samba_tool/ou.py
@@ -0,0 +1,291 @@
+# Unix SMB/CIFS implementation.
+#
+# Copyright (C) Bjoern Baumbach <bb@sernet.de> 2018
+#
+# based on group.py:
+# Copyright (C) Michael Adam 2012
+#
+# 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 os
+import ldb
+from samba.tests.samba_tool.base import SambaToolCmdTest
+
+
+class OUCmdTestCase(SambaToolCmdTest):
+ """Tests for samba-tool ou subcommands"""
+ ous = []
+ samdb = None
+
+ def setUp(self):
+ super().setUp()
+ self.samdb = self.getSamDB("-H", "ldap://%s" % os.environ["DC_SERVER"],
+ "-U%s%%%s" % (os.environ["DC_USERNAME"], os.environ["DC_PASSWORD"]))
+ self.ous = []
+ self.ous.append(self._randomOU({"name": "testou1"}))
+ self.ous.append(self._randomOU({"name": "testou2"}))
+ self.ous.append(self._randomOU({"name": "testou3"}))
+ self.ous.append(self._randomOU({"name": "testou4"}))
+
+ # setup the 4 ous and ensure they are correct
+ for ou in self.ous:
+ (result, out, err) = self._create_ou(ou)
+
+ self.assertCmdSuccess(result, out, err)
+ self.assertEqual(err, "", "There shouldn't be any error message")
+ full_ou_dn = self.samdb.normalize_dn_in_domain("OU=%s" % ou["name"])
+ self.assertIn('Added ou "%s"' % full_ou_dn, out)
+
+ found = self._find_ou(ou["name"])
+
+ self.assertIsNotNone(found)
+
+ self.assertEqual("%s" % found.get("name"), ou["name"])
+ self.assertEqual("%s" % found.get("description"),
+ ou["description"])
+
+ def tearDown(self):
+ super().tearDown()
+ # clean up all the left over ous, just in case
+ for ou in self.ous:
+ if self._find_ou(ou["name"]):
+ (result, out, err) = self.runsubcmd("ou", "delete",
+ "OU=%s" % ou["name"])
+ self.assertCmdSuccess(result, out, err,
+ "Failed to delete ou '%s'" % ou["name"])
+
+ def test_newou(self):
+ """This tests the "ou create" and "ou delete" commands"""
+ # try to create all the ous again, this should fail
+ for ou in self.ous:
+ (result, out, err) = self._create_ou(ou)
+ self.assertCmdFail(result, "Succeeded to add existing ou")
+ self.assertIn("already exists", err)
+
+ # try to delete all the ous we just added
+ for ou in self.ous:
+ (result, out, err) = self.runsubcmd("ou", "delete", "OU=%s" %
+ ou["name"])
+ self.assertCmdSuccess(result, out, err,
+ "Failed to delete ou '%s'" % ou["name"])
+ found = self._find_ou(ou["name"])
+ self.assertIsNone(found,
+ "Deleted ou '%s' still exists" % ou["name"])
+
+ # test creating ous
+ for ou in self.ous:
+ (result, out, err) = self.runsubcmd(
+ "ou", "add", "OU=%s" % ou["name"],
+ "--description=%s" % ou["description"])
+
+ self.assertCmdSuccess(result, out, err)
+ self.assertEqual(err, "", "There shouldn't be any error message")
+ full_ou_dn = self.samdb.normalize_dn_in_domain("OU=%s" % ou["name"])
+ self.assertIn('Added ou "%s"' % full_ou_dn, out)
+
+ found = self._find_ou(ou["name"])
+
+ self.assertEqual("%s" % found.get("ou"),
+ "%s" % ou["name"])
+
+ # try to delete all the ous we just added (with full dn)
+ for ou in self.ous:
+ full_ou_dn = self.samdb.normalize_dn_in_domain("OU=%s" % ou["name"])
+ (result, out, err) = self.runsubcmd("ou", "delete", str(full_ou_dn))
+ self.assertCmdSuccess(result, out, err,
+ "Failed to delete ou '%s'" % ou["name"])
+ found = self._find_ou(ou["name"])
+ self.assertIsNone(found,
+ "Deleted ou '%s' still exists" % ou["name"])
+
+ # test creating ous (with full dn)
+ for ou in self.ous:
+ full_ou_dn = self.samdb.normalize_dn_in_domain("OU=%s" % ou["name"])
+ (result, out, err) = self.runsubcmd(
+ "ou", "add", str(full_ou_dn),
+ "--description=%s" % ou["description"])
+
+ self.assertCmdSuccess(result, out, err)
+ self.assertEqual(err, "", "There shouldn't be any error message")
+ full_ou_dn = self.samdb.normalize_dn_in_domain("OU=%s" % ou["name"])
+ self.assertIn('Added ou "%s"' % full_ou_dn, out)
+
+ found = self._find_ou(ou["name"])
+
+ self.assertEqual("%s" % found.get("ou"),
+ "%s" % ou["name"])
+
+ def test_list(self):
+ (result, out, err) = self.runsubcmd("ou", "list")
+ self.assertCmdSuccess(result, out, err, "Error running list")
+
+ search_filter = "(objectClass=organizationalUnit)"
+
+ oulist = self.samdb.search(base=self.samdb.domain_dn(),
+ scope=ldb.SCOPE_SUBTREE,
+ expression=search_filter,
+ attrs=["name"])
+
+ self.assertTrue(len(oulist) > 0, "no ous found in samdb")
+
+ for ouobj in oulist:
+ name = ouobj.get("name", idx=0)
+ found = self.assertMatch(out, str(name),
+ "ou '%s' not found" % name)
+
+ def test_list_base_dn(self):
+ base_dn = str(self.samdb.domain_dn())
+ (result, out, err) = self.runsubcmd("ou", "list", "-b", base_dn)
+ self.assertCmdSuccess(result, out, err, "Error running list")
+
+ search_filter = "(objectClass=organizationalUnit)"
+
+ oulist = self.samdb.search(base=base_dn,
+ scope=ldb.SCOPE_SUBTREE,
+ expression=search_filter,
+ attrs=["name"])
+
+ self.assertTrue(len(oulist) > 0, "no ous found in samdb")
+
+ for ouobj in oulist:
+ name = ouobj.get("name", idx=0)
+ found = self.assertMatch(out, str(name),
+ "ou '%s' not found" % name)
+
+ def test_rename(self):
+ for ou in self.ous:
+ ousuffix = "RenameTest"
+ newouname = ou["name"] + ousuffix
+ (result, out, err) = self.runsubcmd("ou", "rename",
+ "OU=%s" % ou["name"],
+ "OU=%s" % newouname)
+ self.assertCmdSuccess(result, out, err,
+ "Failed to rename ou '%s'" % ou["name"])
+ found = self._find_ou(ou["name"])
+ self.assertIsNone(found,
+ "Renamed ou '%s' still exists" % ou["name"])
+ found = self._find_ou(newouname)
+ self.assertIsNotNone(found,
+ "Renamed ou '%s' does not exist" % newouname)
+
+ (result, out, err) = self.runsubcmd("ou", "rename",
+ "OU=%s" % newouname,
+ "OU=%s" % ou["name"])
+ self.assertCmdSuccess(result, out, err,
+ "Failed to rename ou '%s'" % newouname)
+
+ def test_move(self):
+ parentou = self._randomOU({"name": "parentOU"})
+ (result, out, err) = self._create_ou(parentou)
+ self.assertCmdSuccess(result, out, err)
+
+ for ou in self.ous:
+ olddn = self._find_ou(ou["name"]).get("dn")
+
+ (result, out, err) = self.runsubcmd("ou", "move",
+ "OU=%s" % ou["name"],
+ "OU=%s" % parentou["name"])
+ self.assertCmdSuccess(result, out, err,
+ "Failed to move ou '%s'" % ou["name"])
+ self.assertEqual(err, "", "There shouldn't be any error message")
+ full_ou_dn = self.samdb.normalize_dn_in_domain("OU=%s" % ou["name"])
+ self.assertIn('Moved ou "%s"' % full_ou_dn, out)
+
+ found = self._find_ou(ou["name"])
+ self.assertNotEqual(found.get("dn"), olddn,
+ "Moved ou '%s' still exists with the same dn" %
+ ou["name"])
+ newexpecteddn = ldb.Dn(self.samdb,
+ "OU=%s,OU=%s,%s" %
+ (ou["name"], parentou["name"],
+ self.samdb.domain_dn()))
+ self.assertEqual(found.get("dn"), newexpecteddn,
+ "Moved ou '%s' does not exist" %
+ ou["name"])
+
+ (result, out, err) = self.runsubcmd("ou", "move",
+ "%s" % newexpecteddn,
+ "%s" % olddn.parent())
+ self.assertCmdSuccess(result, out, err,
+ "Failed to move ou '%s'" % ou["name"])
+
+ (result, out, err) = self.runsubcmd("ou", "delete",
+ "OU=%s" % parentou["name"])
+ self.assertCmdSuccess(result, out, err,
+ "Failed to delete ou '%s'" % parentou["name"])
+
+ def test_listobjects(self):
+ (result, out, err) = self.runsubcmd("ou", "listobjects",
+ "%s" % self.samdb.domain_dn(),
+ "--full-dn")
+ self.assertCmdSuccess(result, out, err,
+ "Failed to list ou's objects")
+ self.assertEqual(err, "", "There shouldn't be any error message")
+
+ objlist = self.samdb.search(base=self.samdb.domain_dn(),
+ scope=ldb.SCOPE_ONELEVEL,
+ attrs=[])
+ self.assertTrue(len(objlist) > 0, "no objects found")
+
+ for obj in objlist:
+ found = self.assertMatch(out, str(obj.dn),
+ "object '%s' not found" % obj.dn)
+
+ def test_list_full_dn(self):
+ (result, out, err) = self.runsubcmd("ou", "list",
+ "--full-dn")
+ self.assertCmdSuccess(result, out, err,
+ "Failed to list ous")
+ self.assertEqual(err, "", "There shouldn't be any error message")
+
+ filter = "(objectClass=organizationalUnit)"
+ objlist = self.samdb.search(base=self.samdb.domain_dn(),
+ scope=ldb.SCOPE_SUBTREE,
+ expression=filter,
+ attrs=[])
+ self.assertTrue(len(objlist) > 0, "no ou objects found")
+
+ for obj in objlist:
+ found = self.assertMatch(out, str(obj.dn),
+ "object '%s' not found" % obj.dn)
+
+ def _randomOU(self, base=None):
+ """create an ou with random attribute values, you can specify base
+ attributes"""
+ if base is None:
+ base = {}
+ ou = {
+ "name": self.randomName(),
+ "description": self.randomName(count=100),
+ }
+ ou.update(base)
+ return ou
+
+ def _create_ou(self, ou):
+ return self.runsubcmd("ou", "add", "OU=%s" % ou["name"],
+ "--description=%s" % ou["description"])
+
+ def _find_ou(self, name):
+ search_filter = ("(&(name=%s)(objectCategory=%s,%s))" %
+ (ldb.binary_encode(name),
+ "CN=Organizational-Unit,CN=Schema,CN=Configuration",
+ self.samdb.domain_dn()))
+ oulist = self.samdb.search(base=self.samdb.domain_dn(),
+ scope=ldb.SCOPE_SUBTREE,
+ expression=search_filter)
+ if oulist:
+ return oulist[0]
+ else:
+ return None
diff --git a/python/samba/tests/samba_tool/passwordsettings.py b/python/samba/tests/samba_tool/passwordsettings.py
new file mode 100644
index 0000000..6db7a58
--- /dev/null
+++ b/python/samba/tests/samba_tool/passwordsettings.py
@@ -0,0 +1,484 @@
+# Test 'samba-tool domain passwordsettings' sub-commands
+#
+# Copyright (C) Andrew Bartlett <abartlet@samba.org> 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 os
+import ldb
+from samba.tests.samba_tool.base import SambaToolCmdTest
+from samba.tests.pso import PasswordSettings, TestUser
+
+
+class PwdSettingsCmdTestCase(SambaToolCmdTest):
+ """Tests for 'samba-tool domain passwordsettings' subcommands"""
+
+ def setUp(self):
+ super().setUp()
+ self.server = "ldap://%s" % os.environ["DC_SERVER"]
+ self.user_auth = "-U%s%%%s" % (os.environ["DC_USERNAME"],
+ os.environ["DC_PASSWORD"])
+ self.ldb = self.getSamDB("-H", self.server, self.user_auth)
+ system_dn = "CN=System,%s" % self.ldb.domain_dn()
+ self.pso_container = "CN=Password Settings Container,%s" % system_dn
+ self.obj_cleanup = []
+
+ def tearDown(self):
+ super().tearDown()
+ # clean-up any objects the test has created
+ for dn in self.obj_cleanup:
+ self.ldb.delete(dn)
+
+ def check_pso(self, pso_name, pso):
+ """Checks the PSO info in the DB matches what's expected"""
+
+ # lookup the PSO in the DB
+ dn = "CN=%s,%s" % (pso_name, self.pso_container)
+ pso_attrs = ['name', 'msDS-PasswordSettingsPrecedence',
+ 'msDS-PasswordReversibleEncryptionEnabled',
+ 'msDS-PasswordHistoryLength',
+ 'msDS-MinimumPasswordLength',
+ 'msDS-PasswordComplexityEnabled',
+ 'msDS-MinimumPasswordAge',
+ 'msDS-MaximumPasswordAge',
+ 'msDS-LockoutObservationWindow',
+ 'msDS-LockoutThreshold', 'msDS-LockoutDuration']
+ res = self.ldb.search(dn, scope=ldb.SCOPE_BASE, attrs=pso_attrs)
+ self.assertEqual(len(res), 1, "PSO lookup failed")
+
+ # convert types in the PSO-settings to what the search returns, i.e.
+ # boolean --> string, seconds --> timestamps in -100 nanosecond units
+ complexity_str = "TRUE" if pso.complexity else "FALSE"
+ plaintext_str = "TRUE" if pso.store_plaintext else "FALSE"
+ lockout_duration = -int(pso.lockout_duration * (1e7))
+ lockout_window = -int(pso.lockout_window * (1e7))
+ min_age = -int(pso.password_age_min * (1e7))
+ max_age = -int(pso.password_age_max * (1e7))
+
+ # check the PSO's settings match the search results
+ self.assertEqual(str(res[0]['msDS-PasswordComplexityEnabled'][0]),
+ complexity_str)
+ plaintext_res = res[0]['msDS-PasswordReversibleEncryptionEnabled'][0]
+ self.assertEqual(str(plaintext_res), plaintext_str)
+ self.assertEqual(int(res[0]['msDS-PasswordHistoryLength'][0]),
+ pso.history_len)
+ self.assertEqual(int(res[0]['msDS-MinimumPasswordLength'][0]),
+ pso.password_len)
+ self.assertEqual(int(res[0]['msDS-MinimumPasswordAge'][0]), min_age)
+ self.assertEqual(int(res[0]['msDS-MaximumPasswordAge'][0]), max_age)
+ self.assertEqual(int(res[0]['msDS-LockoutObservationWindow'][0]),
+ lockout_window)
+ self.assertEqual(int(res[0]['msDS-LockoutDuration'][0]),
+ lockout_duration)
+ self.assertEqual(int(res[0]['msDS-LockoutThreshold'][0]),
+ pso.lockout_attempts)
+ self.assertEqual(int(res[0]['msDS-PasswordSettingsPrecedence'][0]),
+ pso.precedence)
+
+ # check we can also display the PSO via the show command
+ (result, out, err) = self.runsublevelcmd("domain", ("passwordsettings",
+ "pso", "show"), pso_name,
+ "-H", self.server,
+ self.user_auth)
+ self.assertTrue(len(out.split(":")) >= 10,
+ "Expect 10 fields displayed")
+
+ # for a few settings, sanity-check the display is what we expect
+ self.assertIn("Minimum password length: %u" % pso.password_len, out)
+ self.assertIn("Password history length: %u" % pso.history_len, out)
+ lockout_str = "lockout threshold (attempts): %u" % pso.lockout_attempts
+ self.assertIn(lockout_str, out)
+
+ def test_pso_create(self):
+ """Tests basic PSO creation using the samba-tool"""
+
+ # we expect the PSO to take the current domain settings by default
+ # (we'll set precedence/complexity, the rest should be the defaults)
+ expected_pso = PasswordSettings(None, self.ldb)
+ expected_pso.complexity = False
+ expected_pso.precedence = 100
+
+ # check basic PSO creation works
+ pso_name = "test-create-PSO"
+ (result, out, err) = self.runsublevelcmd("domain", ("passwordsettings",
+ "pso", "create"), pso_name,
+ "100", "--complexity=off",
+ "-H", self.server,
+ self.user_auth)
+ # make sure we clean-up after the test completes
+ self.obj_cleanup.append("CN=%s,%s" % (pso_name, self.pso_container))
+
+ self.assertCmdSuccess(result, out, err)
+ self.assertEqual(err, "", "Shouldn't be any error messages")
+ self.assertIn("successfully created", out)
+ self.check_pso(pso_name, expected_pso)
+
+ # check creating a PSO with the same name fails
+ (result, out, err) = self.runsublevelcmd("domain", ("passwordsettings",
+ "pso", "create"), pso_name,
+ "100", "--complexity=off",
+ "-H", self.server,
+ self.user_auth)
+ self.assertCmdFail(result, "Ensure that create for existing PSO fails")
+ self.assertIn("already exists", err)
+
+ # check we need to specify at least one password policy argument
+ pso_name = "test-create-PSO2"
+ (result, out, err) = self.runsublevelcmd("domain", ("passwordsettings",
+ "pso", "create"), pso_name,
+ "100", "-H", self.server,
+ self.user_auth)
+ self.assertCmdFail(result, "Ensure that create for existing PSO fails")
+ self.assertIn("specify at least one password policy setting", err)
+
+ # create a PSO with different settings and check they match
+ expected_pso.complexity = True
+ expected_pso.store_plaintext = True
+ expected_pso.precedence = 50
+ expected_pso.password_len = 12
+ day_in_secs = 60 * 60 * 24
+ expected_pso.password_age_min = 11 * day_in_secs
+ expected_pso.password_age_max = 50 * day_in_secs
+
+ (result, out, err) = self.runsublevelcmd("domain", ("passwordsettings",
+ "pso", "create"), pso_name,
+ "50", "--complexity=on",
+ "--store-plaintext=on",
+ "--min-pwd-length=12",
+ "--min-pwd-age=11",
+ "--max-pwd-age=50",
+ "-H", self.server,
+ self.user_auth)
+ self.obj_cleanup.append("CN=%s,%s" % (pso_name, self.pso_container))
+ self.assertCmdSuccess(result, out, err)
+ self.assertEqual(err, "", "Shouldn't be any error messages")
+ self.assertIn("successfully created", out)
+ self.check_pso(pso_name, expected_pso)
+
+ # check the PSOs we created are present in the 'list' command
+ (result, out, err) = self.runsublevelcmd("domain", ("passwordsettings",
+ "pso", "list"),
+ "-H", self.server,
+ self.user_auth)
+ self.assertCmdSuccess(result, out, err)
+ self.assertIn("test-create-PSO", out)
+ self.assertIn("test-create-PSO2", out)
+
+ def _create_pso(self, pso_name):
+ """Creates a PSO for use in other tests"""
+ # the new PSO will take the current domain settings by default
+ pso_settings = PasswordSettings(None, self.ldb)
+ pso_settings.name = pso_name
+ pso_settings.password_len = 10
+ pso_settings.precedence = 200
+
+ (result, out, err) = self.runsublevelcmd("domain", ("passwordsettings",
+ "pso", "create"), pso_name,
+ "200", "--min-pwd-length=10",
+ "-H", self.server,
+ self.user_auth)
+ # make sure we clean-up after the test completes
+ pso_settings.dn = "CN=%s,%s" % (pso_name, self.pso_container)
+ self.obj_cleanup.append(pso_settings.dn)
+
+ # sanity-check the cmd was successful
+ self.assertCmdSuccess(result, out, err)
+ self.assertEqual(err, "", "Shouldn't be any error messages")
+ self.assertIn("successfully created", out)
+ self.check_pso(pso_name, pso_settings)
+
+ return pso_settings
+
+ def test_pso_set(self):
+ """Tests we can modify a PSO using the samba-tool"""
+
+ pso_name = "test-set-PSO"
+ pso_settings = self._create_pso(pso_name)
+
+ # check we can update a PSO's settings
+ pso_settings.precedence = 99
+ pso_settings.lockout_attempts = 10
+ pso_settings.lockout_duration = 60 * 17
+ (res, out, err) = self.runsublevelcmd("domain", ("passwordsettings",
+ "pso", "set"), pso_name,
+ "--precedence=99",
+ "--account-lockout-threshold=10",
+ "--account-lockout-duration=17",
+ "-H", self.server,
+ self.user_auth)
+ self.assertCmdSuccess(res, out, err)
+ self.assertEqual(err, "", "Shouldn't be any error messages")
+ self.assertIn("Successfully updated", out)
+
+ # check the PSO's settings now reflect the new values
+ self.check_pso(pso_name, pso_settings)
+
+ def test_pso_delete(self):
+ """Tests we can delete a PSO using the samba-tool"""
+
+ pso_name = "test-delete-PSO"
+ self._create_pso(pso_name)
+
+ # check we can successfully delete the PSO
+ (result, out, err) = self.runsublevelcmd("domain", ("passwordsettings",
+ "pso", "delete"), pso_name,
+ "-H", self.server,
+ self.user_auth)
+ self.assertCmdSuccess(result, out, err)
+ self.assertEqual(err, "", "Shouldn't be any error messages")
+ self.assertIn("Deleted PSO", out)
+ dn = "CN=%s,%s" % (pso_name, self.pso_container)
+ self.obj_cleanup.remove(dn)
+
+ # check the object no longer exists in the DB
+ try:
+ self.ldb.search(dn, scope=ldb.SCOPE_BASE, attrs=['name'])
+ self.fail("PSO shouldn't exist")
+ except ldb.LdbError as e:
+ (enum, estr) = e.args
+ self.assertEqual(enum, ldb.ERR_NO_SUCH_OBJECT)
+
+ # run the same cmd again - it should fail because PSO no longer exists
+ (result, out, err) = self.runsublevelcmd("domain", ("passwordsettings",
+ "pso", "delete"), pso_name,
+ "-H", self.server,
+ self.user_auth)
+ self.assertCmdFail(result, "Deleting a non-existent PSO should fail")
+ self.assertIn("Unable to find PSO", err)
+
+ def check_pso_applied(self, user, pso):
+ """Checks that the correct PSO is applied to a given user"""
+
+ # first check the samba-tool output tells us the correct PSO is applied
+ (result, out, err) = self.runsublevelcmd("domain", ("passwordsettings",
+ "pso", "show-user"),
+ user.name, "-H", self.server,
+ self.user_auth)
+ self.assertCmdSuccess(result, out, err)
+ self.assertEqual(err, "", "Shouldn't be any error messages")
+ if pso is None:
+ self.assertIn("No PSO applies to user", out)
+ else:
+ self.assertIn(pso.name, out)
+
+ # then check the DB tells us the same thing
+ if pso is None:
+ self.assertEqual(user.get_resultant_PSO(), None)
+ else:
+ self.assertEqual(user.get_resultant_PSO(), pso.dn)
+
+ def test_pso_apply_to_user(self):
+ """Checks we can apply/unapply a PSO to a user"""
+
+ pso_name = "test-apply-PSO"
+ test_pso = self._create_pso(pso_name)
+
+ # check that a new user has no PSO applied by default
+ user = TestUser("test-PSO-user", self.ldb)
+ self.obj_cleanup.append(user.dn)
+ self.check_pso_applied(user, pso=None)
+
+ # add the user to a new group
+ group_name = "test-PSO-group"
+ dn = "CN=%s,%s" % (group_name, self.ldb.domain_dn())
+ self.ldb.add({"dn": dn, "objectclass": "group",
+ "sAMAccountName": group_name})
+ self.obj_cleanup.append(dn)
+ m = ldb.Message()
+ m.dn = ldb.Dn(self.ldb, dn)
+ m["member"] = ldb.MessageElement(user.dn, ldb.FLAG_MOD_ADD, "member")
+ self.ldb.modify(m)
+
+ # check samba-tool can successfully link a PSO to a group
+ (result, out, err) = self.runsublevelcmd("domain", ("passwordsettings",
+ "pso", "apply"), pso_name,
+ group_name, "-H", self.server,
+ self.user_auth)
+ self.assertCmdSuccess(result, out, err)
+ self.assertEqual(err, "", "Shouldn't be any error messages")
+ self.check_pso_applied(user, pso=test_pso)
+
+ # we should fail if we try to apply the same PSO/group twice though
+ (result, out, err) = self.runsublevelcmd("domain", ("passwordsettings",
+ "pso", "apply"), pso_name,
+ group_name, "-H", self.server,
+ self.user_auth)
+ self.assertCmdFail(result, "Shouldn't be able to apply PSO twice")
+ self.assertIn("already applies", err)
+
+ # check samba-tool can successfully link a PSO to a user
+ (result, out, err) = self.runsublevelcmd("domain", ("passwordsettings",
+ "pso", "apply"), pso_name,
+ user.name, "-H", self.server,
+ self.user_auth)
+ self.assertCmdSuccess(result, out, err)
+ self.assertEqual(err, "", "Shouldn't be any error messages")
+ self.check_pso_applied(user, pso=test_pso)
+
+ # check samba-tool can successfully unlink a group from a PSO
+ (result, out, err) = self.runsublevelcmd("domain", ("passwordsettings",
+ "pso", "unapply"), pso_name,
+ group_name, "-H", self.server,
+ self.user_auth)
+ self.assertCmdSuccess(result, out, err)
+ self.assertEqual(err, "", "Shouldn't be any error messages")
+ # PSO still applies directly to the user, even though group was removed
+ self.check_pso_applied(user, pso=test_pso)
+
+ # check samba-tool can successfully unlink a user from a PSO
+ (result, out, err) = self.runsublevelcmd("domain", ("passwordsettings",
+ "pso", "unapply"), pso_name,
+ user.name, "-H", self.server,
+ self.user_auth)
+ self.assertCmdSuccess(result, out, err)
+ self.assertEqual(err, "", "Shouldn't be any error messages")
+ self.check_pso_applied(user, pso=None)
+
+ def test_pso_unpriv(self):
+ """Checks unprivileged users can't modify PSOs via samba-tool"""
+
+ # create a dummy PSO and a non-admin user
+ pso_name = "test-unpriv-PSO"
+ self._create_pso(pso_name)
+ user = TestUser("test-unpriv-user", self.ldb)
+ self.obj_cleanup.append(user.dn)
+ unpriv_auth = "-U%s%%%s" % (user.name, user.get_password())
+
+ # check we need admin privileges to be able to do anything to PSOs
+ (result, out, err) = self.runsublevelcmd("domain", ("passwordsettings",
+ "pso", "set"), pso_name,
+ "--complexity=off", "-H",
+ self.server, unpriv_auth)
+ self.assertCmdFail(result, "Need admin privileges to modify PSO")
+ self.assertIn("You may not have permission", err)
+
+ (result, out, err) = self.runsublevelcmd("domain", ("passwordsettings",
+ "pso", "create"), "bad-perm",
+ "250", "--complexity=off",
+ "-H", self.server,
+ unpriv_auth)
+ self.assertCmdFail(result, "Need admin privileges to modify PSO")
+ self.assertIn("Administrator permissions are needed", err)
+
+ (result, out, err) = self.runsublevelcmd("domain", ("passwordsettings",
+ "pso", "delete"), pso_name,
+ "-H", self.server,
+ unpriv_auth)
+ self.assertCmdFail(result, "Need admin privileges to delete PSO")
+ self.assertIn("You may not have permission", err)
+
+ (result, out, err) = self.runsublevelcmd("domain", ("passwordsettings",
+ "pso", "show"), pso_name,
+ "-H", self.server,
+ unpriv_auth)
+ self.assertCmdFail(result, "Need admin privileges to view PSO")
+ self.assertIn("You may not have permission", err)
+
+ (result, out, err) = self.runsublevelcmd("domain", ("passwordsettings",
+ "pso", "apply"), pso_name,
+ user.name, "-H", self.server,
+ unpriv_auth)
+ self.assertCmdFail(result, "Need admin privileges to modify PSO")
+ self.assertIn("You may not have permission", err)
+
+ (result, out, err) = self.runsublevelcmd("domain", ("passwordsettings",
+ "pso", "unapply"), pso_name,
+ user.name, "-H", self.server,
+ unpriv_auth)
+ self.assertCmdFail(result, "Need admin privileges to modify PSO")
+ self.assertIn("You may not have permission", err)
+
+ # The 'list' command actually succeeds because it's not easy to tell
+ # whether we got no results due to lack of permissions, or because
+ # there were no PSOs to display
+ (result, out, err) = self.runsublevelcmd("domain", ("passwordsettings",
+ "pso", "list"), "-H",
+ self.server, unpriv_auth)
+ self.assertCmdSuccess(result, out, err)
+ self.assertIn("No PSOs", out)
+ self.assertIn("permission", out)
+
+ def test_domain_passwordsettings(self):
+ """Checks the 'set/show' commands for the domain settings (non-PSO)"""
+
+ # check the 'show' cmd for the domain settings
+ (result, out, err) = self.runsublevelcmd("domain", ("passwordsettings",
+ "show"), "-H", self.server,
+ self.user_auth)
+ self.assertCmdSuccess(result, out, err)
+ self.assertEqual(err, "", "Shouldn't be any error messages")
+
+ # check an arbitrary setting is displayed correctly
+ min_pwd_len = self.ldb.get_minPwdLength()
+ self.assertIn("Minimum password length: %s" % min_pwd_len, out)
+
+ # check we can change the domain setting
+ self.addCleanup(self.ldb.set_minPwdLength, min_pwd_len)
+ new_len = int(min_pwd_len) + 3
+ min_pwd_args = "--min-pwd-length=%u" % new_len
+ (result, out, err) = self.runsublevelcmd("domain", ("passwordsettings",
+ "set"), min_pwd_args,
+ "-H", self.server,
+ self.user_auth)
+ self.assertCmdSuccess(result, out, err)
+ self.assertEqual(err, "", "Shouldn't be any error messages")
+ self.assertIn("successful", out)
+ self.assertEqual(new_len, self.ldb.get_minPwdLength())
+
+ # check the updated value is now displayed
+ (result, out, err) = self.runsublevelcmd("domain", ("passwordsettings",
+ "show"), "-H", self.server,
+ self.user_auth)
+ self.assertCmdSuccess(result, out, err)
+ self.assertEqual(err, "", "Shouldn't be any error messages")
+ self.assertIn("Minimum password length: %u" % new_len, out)
+
+ def test_domain_passwordsettings_pwdage(self):
+ """Checks the 'set' command for the domain password age (non-PSO)"""
+
+ # check we can set the domain max password age
+ max_pwd_age = self.ldb.get_maxPwdAge()
+ self.addCleanup(self.ldb.set_maxPwdAge, max_pwd_age)
+ max_pwd_args = "--max-pwd-age=270"
+ (result, out, err) = self.runsublevelcmd("domain", ("passwordsettings",
+ "set"), max_pwd_args,
+ "-H", self.server,
+ self.user_auth)
+ self.assertCmdSuccess(result, out, err)
+ self.assertEqual(err, "", "Shouldn't be any error messages")
+ self.assertIn("successful", out)
+ self.assertNotEqual(max_pwd_age, self.ldb.get_maxPwdAge())
+
+ # check we can't set the domain min password age to more than the max
+ min_pwd_age = self.ldb.get_minPwdAge()
+ self.addCleanup(self.ldb.set_minPwdAge, min_pwd_age)
+ min_pwd_args = "--min-pwd-age=271"
+ (result, out, err) = self.runsublevelcmd("domain", ("passwordsettings",
+ "set"), min_pwd_args,
+ "-H", self.server,
+ self.user_auth)
+ self.assertCmdFail(result, "minPwdAge > maxPwdAge should be rejected")
+ self.assertIn("Maximum password age", err)
+
+ # check we can set the domain min password age to less than the max
+ min_pwd_args = "--min-pwd-age=269"
+ (result, out, err) = self.runsublevelcmd("domain", ("passwordsettings",
+ "set"), min_pwd_args,
+ "-H", self.server,
+ self.user_auth)
+ self.assertCmdSuccess(result, out, err)
+ self.assertEqual(err, "", "Shouldn't be any error messages")
+ self.assertIn("successful", out)
+ self.assertNotEqual(min_pwd_age, self.ldb.get_minPwdAge())
diff --git a/python/samba/tests/samba_tool/processes.py b/python/samba/tests/samba_tool/processes.py
new file mode 100644
index 0000000..4407797
--- /dev/null
+++ b/python/samba/tests/samba_tool/processes.py
@@ -0,0 +1,42 @@
+# Unix SMB/CIFS implementation.
+# Copyright (C) Andrew Bartlett 2012
+#
+# based on time.py:
+# Copyright (C) Sean Dague <sdague@linux.vnet.ibm.com> 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/>.
+#
+
+import os
+from samba.tests.samba_tool.base import SambaToolCmdTest
+
+
+class ProcessCmdTestCase(SambaToolCmdTest):
+ """Tests for samba-tool process subcommands"""
+
+ def test_name(self):
+ """Run processes command"""
+ (result, out, err) = self.runcmd("processes", "--name", "samba")
+ self.assertCmdSuccess(result, out, err, "Ensuring processes ran successfully")
+
+ def test_unknown_name(self):
+ """Run processes command with an not-existing --name"""
+ (result, out, err) = self.runcmd("processes", "--name", "not-existing-samba")
+ self.assertCmdSuccess(result, out, err, "Ensuring processes ran successfully")
+ self.assertEqual(out, "")
+
+ def test_all(self):
+ """Run processes command"""
+ (result, out, err) = self.runcmd("processes")
+ self.assertCmdSuccess(result, out, err, "Ensuring processes ran successfully")
diff --git a/python/samba/tests/samba_tool/promote_dc_lmdb_size.py b/python/samba/tests/samba_tool/promote_dc_lmdb_size.py
new file mode 100644
index 0000000..88e9d7c
--- /dev/null
+++ b/python/samba/tests/samba_tool/promote_dc_lmdb_size.py
@@ -0,0 +1,174 @@
+# Unix SMB/CIFS implementation.
+# Copyright (C) Catalyst IT Ltd. 2019
+#
+# 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.tests.samba_tool.base import SambaToolCmdTest
+import os
+import shutil
+
+
+class PromoteDcLmdbSizeTestCase(SambaToolCmdTest):
+ """Test setting of the lmdb map size during a promote dc"""
+
+ def setUp(self):
+ super().setUp()
+ self.tempsambadir = os.path.join(self.tempdir, "samba")
+ os.mkdir(self.tempsambadir)
+ (_, name) = os.path.split(self.tempdir)
+ self.netbios_name = name
+
+ # join a domain as a member server
+ #
+ # returns the tuple (ret, stdout, stderr)
+ def join_member(self):
+ command = (
+ "samba-tool " +
+ "domain join " +
+ os.environ["REALM"] + " " +
+ "member " +
+ ("-U%s%%%s " % (os.environ["USERNAME"], os.environ["PASSWORD"])) +
+ ("--option=netbiosname=%s " % self.netbios_name) +
+ ("--targetdir=%s " % self.tempsambadir))
+ return self.run_command(command)
+
+ #
+ # Promote a member server to a domain controller
+ def promote(self, size=None, role=None):
+ command = (
+ "samba-tool " +
+ "domain dcpromo " +
+ os.environ["REALM"] + " " +
+ role + " " +
+ ("-U%s%%%s " % (os.environ["USERNAME"], os.environ["PASSWORD"])) +
+ ("--option=netbiosname=%s " % self.netbios_name) +
+ ("--targetdir=%s " % self.tempsambadir) +
+ "--backend-store=mdb "
+ )
+ if size:
+ command += ("--backend-store-size=%s" % size)
+
+ (ret, stdout, stderr) = self.run_command(command)
+ if ret == 0:
+ self.cleanup_join(self.netbios_name)
+
+ return (ret, stdout, stderr)
+
+ def is_rodc(self):
+ url = "ldb://%s/private/sam.ldb" % self.tempsambadir
+ samdb = self.getSamDB("-H", url)
+ return samdb.am_rodc()
+
+ #
+ # Get the lmdb map size for the specified command
+ #
+ # While there is a python lmdb package available we use the lmdb command
+ # line utilities to avoid introducing a dependency.
+ #
+ def get_lmdb_environment_size(self, path):
+ (result, out, err) = self.run_command("mdb_stat -ne %s" % path)
+ if result:
+ self.fail("Unable to run mdb_stat\n")
+ for line in out.split("\n"):
+ line = line.strip()
+ if line.startswith("Map size:"):
+ line = line.replace(" ", "")
+ (label, size) = line.split(":")
+ return int(size)
+
+ #
+ # Check the lmdb files created by join and ensure that the map size
+ # has been set to size.
+ #
+ # Currently this is all the *.ldb files in private/sam.ldb.d
+ #
+ def check_lmdb_environment_sizes(self, size):
+ directory = os.path.join(self.tempsambadir, "private", "sam.ldb.d")
+ for name in os.listdir(directory):
+ if name.endswith(".ldb"):
+ path = os.path.join(directory, name)
+ s = self.get_lmdb_environment_size(path)
+ if s != size:
+ self.fail("File %s, size=%d larger than %d" %
+ (name, s, size))
+
+ #
+ # Ensure that if --backend-store-size is not specified the default of
+ # 8Gb is used
+ def test_promote_dc_default(self):
+ (result, out, err) = self.join_member()
+ self.assertEqual(0, result)
+ (result, out, err) = self.promote(role="DC")
+ self.assertEqual(0, result)
+ self.check_lmdb_environment_sizes(8 * 1024 * 1024 * 1024)
+ self.assertFalse(self.is_rodc())
+
+ #
+ # Ensure that if --backend-store-size is not specified the default of
+ # 8Gb is used
+ def test_promote_rodc_default(self):
+ (result, out, err) = self.join_member()
+ self.assertEqual(0, result)
+ (result, out, err) = self.promote(role="RODC")
+ self.assertEqual(0, result)
+ self.check_lmdb_environment_sizes(8 * 1024 * 1024 * 1024)
+ self.assertTrue(self.is_rodc())
+
+ #
+ # Promote to a DC with a backend size of 96Mb
+ def test_promote_dc_96Mb(self):
+ (result, out, err) = self.join_member()
+ self.assertEqual(0, result)
+ (result, out, err) = self.promote(role="DC", size="96Mb")
+ self.assertEqual(0, result)
+ self.check_lmdb_environment_sizes(96 * 1024 * 1024)
+ self.assertFalse(self.is_rodc())
+
+ #
+ # Promote to an RODC with a backend size of 256Mb
+ def test_promote_rodc_256Mb(self):
+ (result, out, err) = self.join_member()
+ self.assertEqual(0, result)
+ (result, out, err) = self.promote(role="RODC", size="256Mb")
+ self.assertEqual(0, result)
+ self.check_lmdb_environment_sizes(256 * 1024 * 1024)
+ self.assertTrue(self.is_rodc())
+
+ def test_no_unit_suffix(self):
+ (result, out, err) = self.run_command(
+ 'samba-tool domain dcpromo --backend-store-size "2"')
+ self.assertGreater(result, 0)
+ self.assertRegex(err,
+ r"--backend-store-size invalid suffix ''")
+
+ def test_invalid_unit_suffix(self):
+ (result, out, err) = self.run_command(
+ 'samba-tool domain dcpromo --backend-store-size "2 cd"')
+ self.assertGreater(result, 0)
+ self.assertRegex(err,
+ r"--backend-store-size invalid suffix 'cd'")
+
+ def test_non_numeric(self):
+ (result, out, err) = self.run_command(
+ 'samba-tool domain dcpromo --backend-store-size "two Gb"')
+ self.assertGreater(result, 0)
+ self.assertRegex(
+ err,
+ r"backend-store-size option requires a numeric value, with an"
+ " optional unit suffix")
+
+ def tearDown(self):
+ super().tearDown()
+ shutil.rmtree(self.tempsambadir)
diff --git a/python/samba/tests/samba_tool/provision_lmdb_size.py b/python/samba/tests/samba_tool/provision_lmdb_size.py
new file mode 100644
index 0000000..3514edf
--- /dev/null
+++ b/python/samba/tests/samba_tool/provision_lmdb_size.py
@@ -0,0 +1,132 @@
+# Unix SMB/CIFS implementation.
+# Copyright (C) Catalyst IT Ltd. 2019
+#
+# 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.tests.samba_tool.base import SambaToolCmdTest
+import os
+import shutil
+
+
+class ProvisionLmdbSizeTestCase(SambaToolCmdTest):
+ """Test setting of the lmdb map size during provision"""
+
+ def setUp(self):
+ super().setUp()
+ self.tempsambadir = os.path.join(self.tempdir, "samba")
+ os.mkdir(self.tempsambadir)
+
+ # provision a domain and set the lmdb map size to size
+ #
+ # returns the tuple (ret, stdout, stderr)
+ def provision(self, size=None):
+ command = (
+ "samba-tool " +
+ "domain provision " +
+ "--realm=foo.example.com " +
+ "--domain=FOO " +
+ ("--targetdir=%s " % self.tempsambadir) +
+ "--backend-store=mdb " +
+ "--use-ntvfs "
+ )
+ if size:
+ command += ("--backend-store-size=%s" % size)
+
+ return self.run_command(command)
+
+ #
+ # Get the lmdb map size for the specified command
+ #
+ # While there is a python lmdb package available we use the lmdb command
+ # line utilities to avoid introducing a dependency.
+ #
+ def get_lmdb_environment_size(self, path):
+ (result, out, err) = self.run_command("mdb_stat -ne %s" % path)
+ if result:
+ self.fail("Unable to run mdb_stat\n")
+ for line in out.split("\n"):
+ line = line.strip()
+ if line.startswith("Map size:"):
+ line = line.replace(" ", "")
+ (label, size) = line.split(":")
+ return int(size)
+
+ #
+ # Check the lmdb files created by provision and ensure that the map size
+ # has been set to size.
+ #
+ # Currently this is all the *.ldb files in private/sam.ldb.d
+ #
+ def check_lmdb_environment_sizes(self, size):
+ directory = os.path.join(self.tempsambadir, "private", "sam.ldb.d")
+ for name in os.listdir(directory):
+ if name.endswith(".ldb"):
+ path = os.path.join(directory, name)
+ s = self.get_lmdb_environment_size(path)
+ if s != size:
+ self.fail("File %s, size=%d larger than %d" %
+ (name, s, size))
+
+ #
+ # Ensure that if --backend-store-size is not specified the default of
+ # 8Gb is used
+ def test_default(self):
+ (result, out, err) = self.provision()
+ self.assertEqual(0, result)
+ self.check_lmdb_environment_sizes(8 * 1024 * 1024 * 1024)
+
+ def test_64Mb(self):
+ (result, out, err) = self.provision("64Mb")
+ self.assertEqual(0, result)
+ self.check_lmdb_environment_sizes(64 * 1024 * 1024)
+
+ def test_1Gb(self):
+ (result, out, err) = self.provision("1Gb")
+ self.assertEqual(0, result)
+ self.check_lmdb_environment_sizes(1 * 1024 * 1024 * 1024)
+
+ # 128Mb specified in bytes.
+ #
+ def test_134217728b(self):
+ (result, out, err) = self.provision("134217728b")
+ self.assertEqual(0, result)
+ self.check_lmdb_environment_sizes(134217728)
+
+ def test_no_unit_suffix(self):
+ (result, out, err) = self.run_command(
+ 'samba-tool domain provision --backend-store-size "2"')
+ self.assertGreater(result, 0)
+ self.assertRegex(err,
+ r"--backend-store-size invalid suffix ''")
+
+ def test_invalid_unit_suffix(self):
+ (result, out, err) = self.run_command(
+ 'samba-tool domain provision --backend-store-size "2 cd"')
+ self.assertGreater(result, 0)
+ self.assertRegex(err,
+ r"--backend-store-size invalid suffix 'cd'")
+
+ def test_non_numeric(self):
+ (result, out, err) = self.run_command(
+ 'samba-tool domain provision --backend-store-size "two Gb"')
+ self.assertGreater(result, 0)
+ self.assertRegex(
+ err,
+ r"backend-store-size option requires a numeric value, with an"
+ " optional unit suffix")
+
+ def tearDown(self):
+ super().tearDown()
+ shutil.rmtree(self.tempsambadir)
diff --git a/python/samba/tests/samba_tool/provision_password_check.py b/python/samba/tests/samba_tool/provision_password_check.py
new file mode 100644
index 0000000..51b4a4d
--- /dev/null
+++ b/python/samba/tests/samba_tool/provision_password_check.py
@@ -0,0 +1,57 @@
+# Unix SMB/CIFS implementation.
+# Copyright (C) Catalyst IT Ltd. 2017
+#
+# 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.tests.samba_tool.base import SambaToolCmdTest
+import os
+import shutil
+
+
+class ProvisionPasswordTestCase(SambaToolCmdTest):
+ """Test for password validation in domain provision subcommand"""
+
+ def setUp(self):
+ super().setUp()
+ self.tempsambadir = os.path.join(self.tempdir, "samba")
+ os.mkdir(self.tempsambadir)
+
+ def _provision_with_password(self, password):
+ return self.runsubcmd(
+ "domain", "provision", "--realm=foo.example.com", "--domain=FOO",
+ "--targetdir=%s" % self.tempsambadir, "--adminpass=%s" % password,
+ "--use-ntvfs")
+
+ def test_short_and_low_quality(self):
+ (result, out, err) = self._provision_with_password("foo")
+ self.assertCmdFail(result)
+
+ def test_short(self):
+ (result, out, err) = self._provision_with_password("Fo0!_9")
+ self.assertCmdFail(result)
+ self.assertRegex(err, r"minimum password length")
+
+ def test_low_quality(self):
+ (result, out, err) = self._provision_with_password("aaaaaaaaaaaaaaaaa")
+ self.assertCmdFail(result)
+ self.assertRegex(err, r"quality standards")
+
+ def test_good(self):
+ (result, out, err) = self._provision_with_password("Fo0!_9.")
+ self.assertCmdSuccess(result, out, err)
+
+ def tearDown(self):
+ super().tearDown()
+ shutil.rmtree(self.tempsambadir)
diff --git a/python/samba/tests/samba_tool/provision_userPassword_crypt.py b/python/samba/tests/samba_tool/provision_userPassword_crypt.py
new file mode 100644
index 0000000..2de8cdd
--- /dev/null
+++ b/python/samba/tests/samba_tool/provision_userPassword_crypt.py
@@ -0,0 +1,67 @@
+# Unix SMB/CIFS implementation.
+# Copyright (C) Catalyst IT Ltd. 2021
+#
+# based on provision_lmdb_size.py:
+# Copyright (C) Catalyst IT Ltd. 2019
+#
+# 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.tests.samba_tool.base import SambaToolCmdTest
+import os
+import shutil
+
+
+class ProvisionUserPasswordTestCase(SambaToolCmdTest):
+ """Test for crypt() hashed passwords"""
+
+ def setUp(self):
+ super().setUp()
+ self.tempsambadir = os.path.join(self.tempdir, "samba")
+ os.mkdir(self.tempsambadir)
+
+ # provision a domain
+ #
+ # returns the tuple (ret, stdout, stderr)
+ def provision(self, machinepass=None):
+ command = (
+ "samba-tool " +
+ "domain provision " +
+ "--use-rfc230 " +
+ "--realm=\"EXAMPLE.COM\" " +
+ "--domain=\"EXAMPLE\" " +
+ "--adminpass=\"FooBar123\" " +
+ "--server-role=dc " +
+ "--host-ip=10.166.183.55 " +
+ "--option=\"password hash userPassword " +
+ "schemes=CryptSHA256 CryptSHA512\" " +
+ ("--targetdir=\"%s\" " % self.tempsambadir) +
+ "--use-ntvfs"
+ )
+ if machinepass:
+ command += ("--machinepass=\"%s\"" % machinepass)
+
+ return self.run_command(command)
+
+ def test_crypt(self):
+ (result, out, err) = self.provision()
+ self.assertEqual(0, result)
+
+ def test_length(self):
+ (result, out, err) = self.provision(machinepass="FooBar123" + ("a"*1024))
+ self.assertNotEqual(0, result)
+
+ def tearDown(self):
+ super().tearDown()
+ shutil.rmtree(self.tempsambadir)
diff --git a/python/samba/tests/samba_tool/rodc.py b/python/samba/tests/samba_tool/rodc.py
new file mode 100644
index 0000000..94e84d6
--- /dev/null
+++ b/python/samba/tests/samba_tool/rodc.py
@@ -0,0 +1,131 @@
+# Unix SMB/CIFS implementation.
+# Copyright (C) Catalyst IT Ltd. 2015
+#
+# 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 os
+import ldb
+import samba
+from samba.samdb import SamDB
+from samba.tests import delete_force
+from samba.tests.samba_tool.base import SambaToolCmdTest
+from samba.credentials import Credentials
+from samba.auth import system_session
+
+
+class RodcCmdTestCase(SambaToolCmdTest):
+ def setUp(self):
+ super().setUp()
+ self.lp = samba.param.LoadParm()
+ self.lp.load(os.environ["SMB_CONF_PATH"])
+ self.creds = Credentials()
+ self.creds.set_username(os.environ["DC_USERNAME"])
+ self.creds.set_password(os.environ["DC_PASSWORD"])
+ self.creds.guess(self.lp)
+ self.session = system_session()
+ self.ldb = SamDB("ldap://" + os.environ["DC_SERVER"],
+ session_info=self.session, credentials=self.creds, lp=self.lp)
+
+ self.base_dn = self.ldb.domain_dn()
+
+ self.ldb.newuser("sambatool1", "1qazXSW@")
+ self.ldb.newuser("sambatool2", "2wsxCDE#")
+ self.ldb.newuser("sambatool3", "3edcVFR$")
+ self.ldb.newuser("sambatool4", "4rfvBGT%")
+ self.ldb.newuser("sambatool5", "5tjbNHY*")
+ self.ldb.newuser("sambatool6", "6yknMJU*")
+
+ self.ldb.add_remove_group_members("Allowed RODC Password Replication Group",
+ ["sambatool1", "sambatool2", "sambatool3",
+ "sambatool4", "sambatool5"],
+ add_members_operation=True)
+
+ def tearDown(self):
+ super().tearDown()
+ self.ldb.deleteuser("sambatool1")
+ self.ldb.deleteuser("sambatool2")
+ self.ldb.deleteuser("sambatool3")
+ self.ldb.deleteuser("sambatool4")
+ self.ldb.deleteuser("sambatool5")
+ self.ldb.deleteuser("sambatool6")
+ (result, out, err) = self.runsubcmd("drs", "replicate", "--local", "unused",
+ os.environ["DC_SERVER"], self.base_dn)
+
+ def test_single_by_account_name(self):
+ (result, out, err) = self.runsubcmd("rodc", "preload", "sambatool1",
+ "--server", os.environ["DC_SERVER"])
+ self.assertCmdSuccess(result, out, err, "ensuring rodc prefetch ran successfully")
+ self.assertEqual(out, "Replicating DN CN=sambatool1,CN=Users,%s\n" % self.base_dn)
+ self.assertEqual(err, "")
+
+ def test_single_by_dn(self):
+ (result, out, err) = self.runsubcmd("rodc", "preload", "cn=sambatool2,cn=users,%s" % self.base_dn,
+ "--server", os.environ["DC_SERVER"])
+ self.assertCmdSuccess(result, out, err, "ensuring rodc prefetch ran successfully")
+ self.assertEqual(out, "Replicating DN CN=sambatool2,CN=Users,%s\n" % self.base_dn)
+
+ def test_multi_by_account_name(self):
+ (result, out, err) = self.runsubcmd("rodc", "preload", "sambatool1", "sambatool2",
+ "--server", os.environ["DC_SERVER"])
+ self.assertCmdSuccess(result, out, err, "ensuring rodc prefetch ran successfully")
+ self.assertEqual(out, "Replicating DN CN=sambatool1,CN=Users,%s\nReplicating DN CN=sambatool2,CN=Users,%s\n" % (self.base_dn, self.base_dn))
+
+ def test_multi_by_dn(self):
+ (result, out, err) = self.runsubcmd("rodc", "preload", "cn=sambatool3,cn=users,%s" % self.base_dn, "cn=sambatool4,cn=users,%s" % self.base_dn,
+ "--server", os.environ["DC_SERVER"])
+ self.assertCmdSuccess(result, out, err, "ensuring rodc prefetch ran successfully")
+ self.assertEqual(out, "Replicating DN CN=sambatool3,CN=Users,%s\nReplicating DN CN=sambatool4,CN=Users,%s\n" % (self.base_dn, self.base_dn))
+
+ def test_multi_in_file(self):
+ tempf = os.path.join(self.tempdir, "accountlist")
+ open(tempf, 'w').write("sambatool1\nsambatool2")
+ (result, out, err) = self.runsubcmd("rodc", "preload", "--file", tempf,
+ "--server", os.environ["DC_SERVER"])
+ self.assertCmdSuccess(result, out, err, "ensuring rodc prefetch ran successfully")
+ self.assertEqual(out, "Replicating DN CN=sambatool1,CN=Users,%s\nReplicating DN CN=sambatool2,CN=Users,%s\n" % (self.base_dn, self.base_dn))
+ os.unlink(tempf)
+
+ def test_multi_with_missing_name_success(self):
+ (result, out, err) = self.runsubcmd("rodc", "preload",
+ "nonexistentuser1", "sambatool5",
+ "nonexistentuser2",
+ "--server", os.environ["DC_SERVER"],
+ "--ignore-errors")
+ self.assertCmdSuccess(result, out, err, "ensuring rodc prefetch ran successfully")
+ self.assertTrue(out.startswith("Replicating DN CN=sambatool5,CN=Users,%s\n"
+ % self.base_dn))
+
+ def test_multi_with_missing_name_failure(self):
+ (result, out, err) = self.runsubcmd("rodc", "preload",
+ "nonexistentuser1", "sambatool5",
+ "nonexistentuser2",
+ "--server", os.environ["DC_SERVER"])
+ self.assertCmdFail(result, "ensuring rodc prefetch quit on missing user")
+
+ def test_multi_without_group_success(self):
+ (result, out, err) = self.runsubcmd("rodc", "preload",
+ "sambatool6", "sambatool5",
+ "--server", os.environ["DC_SERVER"],
+ "--ignore-errors")
+ self.assertCmdSuccess(result, out, err, "ensuring rodc prefetch ran successfully")
+ self.assertTrue(out.startswith("Replicating DN CN=sambatool6,CN=Users,%s\n"
+ "Replicating DN CN=sambatool5,CN=Users,%s\n"
+ % (self.base_dn, self.base_dn)))
+
+ def test_multi_without_group_failure(self):
+ (result, out, err) = self.runsubcmd("rodc", "preload",
+ "sambatool6", "sambatool5",
+ "--server", os.environ["DC_SERVER"])
+ self.assertCmdFail(result, "ensuring rodc prefetch quit on non-replicated user")
diff --git a/python/samba/tests/samba_tool/schema.py b/python/samba/tests/samba_tool/schema.py
new file mode 100644
index 0000000..5c4ac78
--- /dev/null
+++ b/python/samba/tests/samba_tool/schema.py
@@ -0,0 +1,109 @@
+# Unix SMB/CIFS implementation.
+# Copyright (C) William Brown <william@blackhats.net.au> 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 os
+import ldb
+from samba.tests.samba_tool.base import SambaToolCmdTest
+
+
+class SchemaCmdTestCase(SambaToolCmdTest):
+ """Tests for samba-tool dsacl subcommands"""
+ samdb = None
+
+ def setUp(self):
+ super().setUp()
+ self.samdb = self.getSamDB("-H", "ldap://%s" % os.environ["DC_SERVER"],
+ "-U%s%%%s" % (os.environ["DC_USERNAME"], os.environ["DC_PASSWORD"]))
+
+ def test_display_attribute(self):
+ """Tests that we can display schema attributes"""
+ (result, out, err) = self.runsublevelcmd("schema", ("attribute",
+ "show"), "uid",
+ "-H", "ldap://%s" % os.environ["DC_SERVER"],
+ "-U%s%%%s" % (os.environ["DC_USERNAME"],
+ os.environ["DC_PASSWORD"]))
+
+ self.assertCmdSuccess(result, out, err)
+ self.assertEqual(err, "", "Shouldn't be any error messages")
+ self.assertIn("dn: CN=uid,CN=Schema,CN=Configuration,", out)
+
+ def test_modify_attribute_searchflags(self):
+ """Tests that we can modify searchFlags of an attribute"""
+ (result, out, err) = self.runsublevelcmd("schema", ("attribute",
+ "modify"), "uid", "--searchflags=9",
+ "-H", "ldap://%s" % os.environ["DC_SERVER"],
+ "-U%s%%%s" % (os.environ["DC_USERNAME"],
+ os.environ["DC_PASSWORD"]))
+
+ self.assertCmdFail(result, 'Unknown flag 9, please see --help')
+
+ (result, out, err) = self.runsublevelcmd("schema", ("attribute",
+ "modify"), "uid", "--searchflags=fATTINDEX",
+ "-H", "ldap://%s" % os.environ["DC_SERVER"],
+ "-U%s%%%s" % (os.environ["DC_USERNAME"],
+ os.environ["DC_PASSWORD"]))
+
+ self.assertCmdSuccess(result, out, err)
+ self.assertEqual(err, "", "Shouldn't be any error messages")
+ self.assertIn("modified cn=uid,CN=Schema,CN=Configuration,", out)
+
+ (result, out, err) = self.runsublevelcmd("schema", ("attribute",
+ "modify"), "uid",
+ "--searchflags=fATTINDEX,fSUBTREEATTINDEX",
+ "-H", "ldap://%s" % os.environ["DC_SERVER"],
+ "-U%s%%%s" % (os.environ["DC_USERNAME"],
+ os.environ["DC_PASSWORD"]))
+
+ self.assertCmdSuccess(result, out, err)
+ self.assertEqual(err, "", "Shouldn't be any error messages")
+ self.assertIn("modified cn=uid,CN=Schema,CN=Configuration,", out)
+
+ (result, out, err) = self.runsublevelcmd("schema", ("attribute",
+ "modify"), "uid",
+ "--searchflags=fAtTiNdEx,fPRESERVEONDELETE",
+ "-H", "ldap://%s" % os.environ["DC_SERVER"],
+ "-U%s%%%s" % (os.environ["DC_USERNAME"],
+ os.environ["DC_PASSWORD"]))
+
+ self.assertCmdSuccess(result, out, err)
+ self.assertEqual(err, "", "Shouldn't be any error messages")
+ self.assertIn("modified cn=uid,CN=Schema,CN=Configuration,", out)
+
+ def test_show_oc_attribute(self):
+ """Tests that we can modify searchFlags of an attribute"""
+ (result, out, err) = self.runsublevelcmd("schema", ("attribute",
+ "show_oc"), "cn",
+ "-H", "ldap://%s" % os.environ["DC_SERVER"],
+ "-U%s%%%s" % (os.environ["DC_USERNAME"],
+ os.environ["DC_PASSWORD"]))
+
+ self.assertCmdSuccess(result, out, err)
+ self.assertEqual(err, "", "Shouldn't be any error messages")
+ self.assertIn("--- MAY contain ---", out)
+ self.assertIn("--- MUST contain ---", out)
+
+ def test_display_objectclass(self):
+ """Tests that we can display schema objectclasses"""
+ (result, out, err) = self.runsublevelcmd("schema", ("objectclass",
+ "show"), "person",
+ "-H", "ldap://%s" % os.environ["DC_SERVER"],
+ "-U%s%%%s" % (os.environ["DC_USERNAME"],
+ os.environ["DC_PASSWORD"]))
+
+ self.assertCmdSuccess(result, out, err)
+ self.assertEqual(err, "", "Shouldn't be any error messages")
+ self.assertIn("dn: CN=Person,CN=Schema,CN=Configuration,", out)
diff --git a/python/samba/tests/samba_tool/silo_base.py b/python/samba/tests/samba_tool/silo_base.py
new file mode 100644
index 0000000..451d330
--- /dev/null
+++ b/python/samba/tests/samba_tool/silo_base.py
@@ -0,0 +1,229 @@
+# Unix SMB/CIFS implementation.
+#
+# Base test class for samba-tool domain auth policy and silo commands.
+#
+# Copyright (C) Catalyst.Net Ltd. 2023
+#
+# Written by Rob van der Linde <rob@catalyst.net.nz>
+#
+# 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 os
+
+from ldb import SCOPE_ONELEVEL
+
+from samba.netcmd.domain.models import Group
+
+from .base import SambaToolCmdTest
+
+HOST = "ldap://{DC_SERVER}".format(**os.environ)
+CREDS = "-U{DC_USERNAME}%{DC_PASSWORD}".format(**os.environ)
+
+
+class SiloTest(SambaToolCmdTest):
+ """Base test class for silo and policy related commands."""
+
+ @classmethod
+ def setUpClass(cls):
+ cls.samdb = cls.getSamDB("-H", HOST, CREDS)
+ super().setUpClass()
+
+ @classmethod
+ def setUpTestData(cls):
+ cls.create_authentication_policy(name="User Policy")
+ cls.create_authentication_policy(name="Service Policy")
+ cls.create_authentication_policy(name="Computer Policy")
+
+ cls.create_authentication_silo(
+ name="Developers",
+ description="Developers, Developers, Developers!",
+ user_authentication_policy="User Policy")
+ cls.create_authentication_silo(
+ name="Managers",
+ description="Managers",
+ user_authentication_policy="User Policy")
+ cls.create_authentication_silo(
+ name="QA",
+ description="Quality Assurance",
+ user_authentication_policy="User Policy",
+ service_authentication_policy="Service Policy",
+ computer_authentication_policy="Computer Policy")
+
+ cls.device_group = Group(name="device-group")
+ cls.device_group.save(cls.samdb)
+ cls.addClassCleanup(cls.device_group.delete, cls.samdb)
+
+ def get_services_dn(self):
+ """Returns Services DN."""
+ services_dn = self.samdb.get_config_basedn()
+ services_dn.add_child("CN=Services")
+ return services_dn
+
+ def get_authn_configuration_dn(self):
+ """Returns AuthN Configuration DN."""
+ authn_policy_configuration = self.get_services_dn()
+ authn_policy_configuration.add_child("CN=AuthN Policy Configuration")
+ return authn_policy_configuration
+
+ def get_authn_silos_dn(self):
+ """Returns AuthN Silos DN."""
+ authn_silos_dn = self.get_authn_configuration_dn()
+ authn_silos_dn.add_child("CN=AuthN Silos")
+ return authn_silos_dn
+
+ def get_authn_policies_dn(self):
+ """Returns AuthN Policies DN."""
+ authn_policies_dn = self.get_authn_configuration_dn()
+ authn_policies_dn.add_child("CN=AuthN Policies")
+ return authn_policies_dn
+
+ def get_users_dn(self):
+ """Returns Users DN."""
+ users_dn = self.samdb.get_root_basedn()
+ users_dn.add_child("CN=Users")
+ return users_dn
+
+ def get_user(self, username):
+ """Get a user by username."""
+ users_dn = self.get_users_dn()
+
+ result = self.samdb.search(base=users_dn,
+ scope=SCOPE_ONELEVEL,
+ expression=f"(sAMAccountName={username})")
+
+ if len(result) == 1:
+ return result[0]
+
+ @classmethod
+ def _run(cls, *argv):
+ """Override _run, so we don't always have to pass host and creds."""
+ args = list(argv)
+ args.extend(["-H", HOST, CREDS])
+ return super()._run(*args)
+
+ runcmd = _run
+ runsubcmd = _run
+
+ @classmethod
+ def create_authentication_policy(cls, name, description=None, audit=False,
+ protect=False):
+ """Create an authentication policy."""
+
+ # base command for create authentication policy
+ cmd = ["domain", "auth", "policy", "create", "--name", name]
+
+ # optional attributes
+ if description is not None:
+ cmd.append(f"--description={description}")
+ if audit:
+ cmd.append("--audit")
+ if protect:
+ cmd.append("--protect")
+
+ # Run command and store name in self.silos for tearDownClass to clean
+ # up.
+ result, out, err = cls.runcmd(*cmd)
+ assert result is None
+ assert out.startswith("Created authentication policy")
+ cls.addClassCleanup(cls.delete_authentication_policy,
+ name=name, force=True)
+ return name
+
+ @classmethod
+ def delete_authentication_policy(cls, name, force=False):
+ """Delete authentication policy by name."""
+ cmd = ["domain", "auth", "policy", "delete", "--name", name]
+
+ # Force-delete protected authentication policy.
+ if force:
+ cmd.append("--force")
+
+ result, out, err = cls.runcmd(*cmd)
+ assert result is None
+ assert "Deleted authentication policy" in out
+
+ @classmethod
+ def create_authentication_silo(cls, name, description=None,
+ user_authentication_policy=None,
+ service_authentication_policy=None,
+ computer_authentication_policy=None,
+ audit=False, protect=False):
+ """Create an authentication silo using the samba-tool command."""
+
+ # Base command for create authentication policy.
+ cmd = ["domain", "auth", "silo", "create", "--name", name]
+
+ # Authentication policies.
+ if user_authentication_policy:
+ cmd += ["--user-authentication-policy",
+ user_authentication_policy]
+ if service_authentication_policy:
+ cmd += ["--service-authentication-policy",
+ service_authentication_policy]
+ if computer_authentication_policy:
+ cmd += ["--computer-authentication-policy",
+ computer_authentication_policy]
+
+ # Other optional attributes.
+ if description is not None:
+ cmd.append(f"--description={description}")
+ if protect:
+ cmd.append("--protect")
+ if audit:
+ cmd.append("--audit")
+
+ # Run command and store name in self.silos for tearDownClass to clean
+ # up.
+ result, out, err = cls.runcmd(*cmd)
+ assert result is None
+ assert out.startswith("Created authentication silo")
+ cls.addClassCleanup(cls.delete_authentication_silo,
+ name=name, force=True)
+ return name
+
+ @classmethod
+ def delete_authentication_silo(cls, name, force=False):
+ """Delete authentication silo by name."""
+ cmd = ["domain", "auth", "silo", "delete", "--name", name]
+
+ # Force-delete protected authentication silo.
+ if force:
+ cmd.append("--force")
+
+ result, out, err = cls.runcmd(*cmd)
+ assert result is None
+ assert "Deleted authentication silo" in out
+
+ def get_authentication_silo(self, name):
+ """Get authentication silo by name."""
+ authn_silos_dn = self.get_authn_silos_dn()
+
+ result = self.samdb.search(base=authn_silos_dn,
+ scope=SCOPE_ONELEVEL,
+ expression=f"(CN={name})")
+
+ if len(result) == 1:
+ return result[0]
+
+ def get_authentication_policy(self, name):
+ """Get authentication policy by name."""
+ authn_policies_dn = self.get_authn_policies_dn()
+
+ result = self.samdb.search(base=authn_policies_dn,
+ scope=SCOPE_ONELEVEL,
+ expression=f"(CN={name})")
+
+ if len(result) == 1:
+ return result[0]
diff --git a/python/samba/tests/samba_tool/sites.py b/python/samba/tests/samba_tool/sites.py
new file mode 100644
index 0000000..4288f35
--- /dev/null
+++ b/python/samba/tests/samba_tool/sites.py
@@ -0,0 +1,205 @@
+# Unix SMB/CIFS implementation.
+# Copyright (C) Catalyst.Net LTD 2015
+# Copyright (C) Sean Dague <sdague@linux.vnet.ibm.com> 2011
+#
+# Catalyst.Net's contribution was written by Douglas Bagnall
+# <douglas.bagnall@catalyst.net.nz>.
+#
+# 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 json
+import os
+import ldb
+from samba.tests.samba_tool.base import SambaToolCmdTest
+from samba import sites, subnets
+
+
+class BaseSitesCmdTestCase(SambaToolCmdTest):
+ """Tests for samba-tool sites subnets"""
+ def setUp(self):
+ super().setUp()
+ self.dburl = "ldap://%s" % os.environ["DC_SERVER"]
+ self.creds_string = "-U%s%%%s" % (os.environ["DC_USERNAME"],
+ os.environ["DC_PASSWORD"])
+
+ self.samdb = self.getSamDB("-H", self.dburl, self.creds_string)
+ self.config_dn = str(self.samdb.get_config_basedn())
+
+
+class SitesCmdTestCase(BaseSitesCmdTestCase):
+
+ def test_site_create(self):
+ sitename = 'new_site'
+
+ result, out, err = self.runsubcmd("sites", "create", sitename,
+ "-H", self.dburl, self.creds_string)
+ self.assertCmdSuccess(result, out, err)
+
+ dnsites = ldb.Dn(self.samdb, "CN=Sites,%s" % self.config_dn)
+ dnsite = ldb.Dn(self.samdb, "CN=%s,%s" % (sitename, dnsites))
+
+ ret = self.samdb.search(base=dnsites, scope=ldb.SCOPE_ONELEVEL,
+ expression='(cn=%s)' % sitename)
+ self.assertEqual(len(ret), 1)
+
+ # now delete it
+ self.samdb.delete(dnsite, ["tree_delete:0"])
+
+ def test_site_list(self):
+ result, out, err = self.runsubcmd("sites", "list",
+ "-H", self.dburl, self.creds_string)
+ self.assertCmdSuccess(result, out, err)
+ self.assertIn("Default-First-Site-Name", out)
+
+ # The same but with --json
+ result, out, err = self.runsubcmd("sites", "list", "--json",
+ "-H", self.dburl, self.creds_string)
+ self.assertCmdSuccess(result, out, err)
+ json_data = json.loads(out)
+ self.assertIn("Default-First-Site-Name", json_data)
+
+ def test_site_view(self):
+ result, out, err = self.runsubcmd("sites", "view",
+ "Default-First-Site-Name",
+ "-H", self.dburl, self.creds_string)
+ self.assertCmdSuccess(result, out, err)
+ json_data = json.loads(out)
+ self.assertEqual(json_data["cn"], "Default-First-Site-Name")
+
+ # Now try one that doesn't exist
+ result, out, err = self.runsubcmd("sites", "view",
+ "Does-Not-Exist",
+ "-H", self.dburl, self.creds_string)
+ self.assertCmdFail(result, err)
+
+
+class SitesSubnetCmdTestCase(BaseSitesCmdTestCase):
+ def setUp(self):
+ super().setUp()
+ self.sitename = "testsite"
+ self.sitename2 = "testsite2"
+ self.samdb.transaction_start()
+ sites.create_site(self.samdb, self.config_dn, self.sitename)
+ sites.create_site(self.samdb, self.config_dn, self.sitename2)
+ self.samdb.transaction_commit()
+
+ def tearDown(self):
+ self.samdb.transaction_start()
+ sites.delete_site(self.samdb, self.config_dn, self.sitename)
+ sites.delete_site(self.samdb, self.config_dn, self.sitename2)
+ self.samdb.transaction_commit()
+ super().tearDown()
+
+ def test_site_subnet_create(self):
+ cidrs = (("10.9.8.0/24", self.sitename),
+ ("50.60.0.0/16", self.sitename2),
+ ("50.61.0.0/16", self.sitename2), # second subnet on the site
+ ("50.0.0.0/8", self.sitename), # overlapping subnet, other site
+ ("50.62.1.2/32", self.sitename), # single IP
+ ("aaaa:bbbb:cccc:dddd:eeee:ffff:2222:1100/120",
+ self.sitename2),
+ )
+
+ for cidr, sitename in cidrs:
+ result, out, err = self.runsubcmd("sites", "subnet", "create",
+ cidr, sitename,
+ "-H", self.dburl,
+ self.creds_string)
+ self.assertCmdSuccess(result, out, err)
+
+ ret = self.samdb.search(base=self.config_dn,
+ scope=ldb.SCOPE_SUBTREE,
+ expression=('(&(objectclass=subnet)(cn=%s))'
+ % cidr))
+ self.assertIsNotNone(ret)
+ self.assertEqual(len(ret), 1)
+
+ dnsubnets = ldb.Dn(self.samdb,
+ "CN=Subnets,CN=Sites,%s" % self.config_dn)
+
+ for cidr, sitename in cidrs:
+ dnsubnet = ldb.Dn(self.samdb, ("Cn=%s,CN=Subnets,CN=Sites,%s" %
+ (cidr, self.config_dn)))
+
+ ret = self.samdb.search(base=dnsubnets, scope=ldb.SCOPE_ONELEVEL,
+ expression='(CN=%s)' % cidr)
+ self.assertIsNotNone(ret)
+ self.assertEqual(len(ret), 1)
+ self.samdb.delete(dnsubnet, ["tree_delete:0"])
+
+ def test_site_subnet_create_should_fail(self):
+ cidrs = (("10.9.8.0/33", self.sitename), # mask too big
+ ("50.60.0.0/8", self.sitename2), # insufficient zeros
+ ("50.261.0.0/16", self.sitename2), # bad octet
+ ("7.0.0.0.0/0", self.sitename), # insufficient zeros
+ ("aaaa:bbbb:cccc:dddd:eeee:ffff:2222:1100/119",
+ self.sitename), # insufficient zeros
+ )
+
+ for cidr, sitename in cidrs:
+ result, out, err = self.runsubcmd("sites", "subnet", "create",
+ cidr, sitename,
+ "-H", self.dburl,
+ self.creds_string)
+ self.assertCmdFail(result)
+
+ ret = self.samdb.search(base=self.config_dn,
+ scope=ldb.SCOPE_SUBTREE,
+ expression=('(&(objectclass=subnet)(cn=%s))'
+ % cidr))
+
+ self.assertIsNotNone(ret)
+ self.assertEqual(len(ret), 0)
+
+ def test_site_subnet_list(self):
+ subnet = "10.9.8.0/24"
+ subnets.create_subnet(self.samdb, self.samdb.get_config_basedn(),
+ subnet, self.sitename)
+
+ # cleanup after test
+ dnsubnet = ldb.Dn(self.samdb, ("CN=%s,CN=Subnets,CN=Sites,%s" %
+ (subnet, self.config_dn)))
+ self.addCleanup(self.samdb.delete, dnsubnet, ["tree_delete:1"])
+
+ result, out, err = self.runsubcmd("sites", "subnet", "list",
+ self.sitename,
+ "-H", self.dburl, self.creds_string)
+
+ self.assertCmdSuccess(result, out, err)
+ self.assertIn(subnet, out)
+
+ def test_site_subnet_view(self):
+ subnet = "50.60.0.0/16"
+ subnets.create_subnet(self.samdb, self.samdb.get_config_basedn(),
+ subnet, self.sitename2)
+
+ # cleanup after test
+ dnsubnet = ldb.Dn(self.samdb, ("CN=%s,CN=Subnets,CN=Sites,%s" %
+ (subnet, self.config_dn)))
+ self.addCleanup(self.samdb.delete, dnsubnet, ["tree_delete:1"])
+
+ result, out, err = self.runsubcmd("sites", "subnet",
+ "view", subnet,
+ "-H", self.dburl, self.creds_string)
+
+ self.assertCmdSuccess(result, out, err)
+ json_data = json.loads(out)
+ self.assertEqual(json_data["cn"], subnet)
+
+ # Now try one that doesn't exist
+ result, out, err = self.runsubcmd("sites", "subnet",
+ "view", "50.0.0.0/8",
+ "-H", self.dburl, self.creds_string)
+ self.assertCmdFail(result, err)
diff --git a/python/samba/tests/samba_tool/timecmd.py b/python/samba/tests/samba_tool/timecmd.py
new file mode 100644
index 0000000..8e286f6
--- /dev/null
+++ b/python/samba/tests/samba_tool/timecmd.py
@@ -0,0 +1,44 @@
+# Unix SMB/CIFS implementation.
+# Copyright (C) Sean Dague <sdague@linux.vnet.ibm.com> 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/>.
+#
+
+import os
+from time import localtime, strptime, mktime
+from samba.tests.samba_tool.base import SambaToolCmdTest
+
+
+class TimeCmdTestCase(SambaToolCmdTest):
+ """Tests for samba-tool time subcommands"""
+
+ def test_timeget(self):
+ """Run time against the server and make sure it looks accurate"""
+ (result, out, err) = self.runcmd("time", os.environ["SERVER"])
+ self.assertCmdSuccess(result, out, err, "Ensuring time ran successfully")
+
+ timefmt = strptime(out, "%a %b %d %H:%M:%S %Y %Z\n")
+ servertime = int(mktime(timefmt))
+ now = int(mktime(localtime()))
+
+ # because there is a race here, allow up to 5 seconds difference in times
+ delta = 5
+ self.assertTrue((servertime > (now - delta) and (servertime < (now + delta)), "Time is now"))
+
+ def test_timefail(self):
+ """Run time against a non-existent server, and make sure it fails"""
+ (result, out, err) = self.runcmd("time", "notaserver")
+ self.assertEqual(result, -1, "check for result code")
+ self.assertNotEqual(err.strip().find("NT_STATUS_OBJECT_NAME_NOT_FOUND"), -1, "ensure right error string")
+ self.assertEqual(out, "", "ensure no output returned")
diff --git a/python/samba/tests/samba_tool/user.py b/python/samba/tests/samba_tool/user.py
new file mode 100644
index 0000000..26c9748
--- /dev/null
+++ b/python/samba/tests/samba_tool/user.py
@@ -0,0 +1,1246 @@
+# Unix SMB/CIFS implementation.
+# Copyright (C) Sean Dague <sdague@linux.vnet.ibm.com> 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/>.
+#
+
+import os
+import time
+import base64
+import ldb
+from samba.tests.samba_tool.base import SambaToolCmdTest
+from samba import (
+ credentials,
+ nttime2unix,
+ dsdb,
+ werror,
+ )
+from samba.ndr import ndr_unpack
+from samba.dcerpc import drsblobs
+from samba.common import get_bytes
+from samba.common import get_string
+from samba.tests import env_loadparm
+
+
+class UserCmdTestCase(SambaToolCmdTest):
+ """Tests for samba-tool user subcommands"""
+ users = []
+ samdb = None
+
+ def setUp(self):
+ super().setUp()
+ self.samdb = self.getSamDB("-H", "ldap://%s" % os.environ["DC_SERVER"],
+ "-U%s%%%s" % (os.environ["DC_USERNAME"], os.environ["DC_PASSWORD"]))
+
+ # Modify the default template homedir
+ lp = self.get_loadparm()
+ self.template_homedir = lp.get('template homedir')
+ lp.set('template homedir', '/home/test/%D/%U')
+
+ self.users = []
+ self.users.append(self._randomUser({"name": "sambatool1", "company": "comp1"}))
+ self.users.append(self._randomUser({"name": "sambatool2", "company": "comp1"}))
+ self.users.append(self._randomUser({"name": "sambatool3", "company": "comp2"}))
+ self.users.append(self._randomUser({"name": "sambatool4", "company": "comp2"}))
+ self.users.append(self._randomPosixUser({"name": "posixuser1"}))
+ self.users.append(self._randomPosixUser({"name": "posixuser2"}))
+ self.users.append(self._randomPosixUser({"name": "posixuser3"}))
+ self.users.append(self._randomPosixUser({"name": "posixuser4"}))
+ self.users.append(self._randomUnixUser({"name": "unixuser1"}))
+ self.users.append(self._randomUnixUser({"name": "unixuser2"}))
+ self.users.append(self._randomUnixUser({"name": "unixuser3"}))
+ self.users.append(self._randomUnixUser({"name": "unixuser4"}))
+
+ # Make sure users don't exist
+ for user in self.users:
+ if self._find_user(user["name"]):
+ self.runsubcmd("user", "delete", user["name"])
+
+ # setup the 12 users and ensure they are correct
+ for user in self.users:
+ (result, out, err) = user["createUserFn"](user)
+
+ self.assertCmdSuccess(result, out, err)
+ self.assertEqual(err, "", "Shouldn't be any error messages")
+ if 'unix' in user["name"]:
+ self.assertIn("Modified User '%s' successfully" % user["name"],
+ out)
+ else:
+ self.assertIn("User '%s' added successfully" % user["name"],
+ out)
+
+ user["checkUserFn"](user)
+
+ def tearDown(self):
+ super().tearDown()
+ # clean up all the left over users, just in case
+ for user in self.users:
+ if self._find_user(user["name"]):
+ self.runsubcmd("user", "delete", user["name"])
+ lp = env_loadparm()
+ # second run of this test
+ # the cache is still there and '--cache-ldb-initialize'
+ # will fail
+ cachedb = lp.private_path("user-syncpasswords-cache.ldb")
+ if os.path.exists(cachedb):
+ os.remove(cachedb)
+ lp.set('template homedir', self.template_homedir)
+
+ def test_newuser(self):
+ # try to add all the users again, this should fail
+ for user in self.users:
+ (result, out, err) = self._create_user(user)
+ self.assertCmdFail(result, "Ensure that create user fails")
+ self.assertIn("LDAP error 68 LDAP_ENTRY_ALREADY_EXISTS", err)
+
+ # try to delete all the 4 users we just added
+ for user in self.users:
+ (result, out, err) = self.runsubcmd("user", "delete", user["name"])
+ self.assertCmdSuccess(result, out, err, "Can we delete users")
+ found = self._find_user(user["name"])
+ self.assertIsNone(found)
+
+ # test adding users with --use-username-as-cn
+ for user in self.users:
+ (result, out, err) = self.runsubcmd("user", "create", user["name"], user["password"],
+ "--use-username-as-cn",
+ "--surname=%s" % user["surname"],
+ "--given-name=%s" % user["given-name"],
+ "--job-title=%s" % user["job-title"],
+ "--department=%s" % user["department"],
+ "--description=%s" % user["description"],
+ "--company=%s" % user["company"],
+ "-H", "ldap://%s" % os.environ["DC_SERVER"],
+ "-U%s%%%s" % (os.environ["DC_USERNAME"], os.environ["DC_PASSWORD"]))
+
+ self.assertCmdSuccess(result, out, err)
+ self.assertEqual(err, "", "Shouldn't be any error messages")
+ self.assertIn("User '%s' added successfully" % user["name"], out)
+
+ found = self._find_user(user["name"])
+
+ self.assertEqual("%s" % found.get("cn"), "%(name)s" % user)
+ self.assertEqual("%s" % found.get("name"), "%(name)s" % user)
+
+ def test_newuser_weak_password(self):
+ # Ensure that when we try to create a user over LDAP (thus no
+ # transactions) and the password is too weak, we do not get a
+ # half-created account.
+
+ def cleanup_user(username):
+ try:
+ self.samdb.deleteuser(username)
+ except Exception as err:
+ estr = err.args[0]
+ if 'Unable to find user' not in estr:
+ raise
+
+ server = os.environ['DC_SERVER']
+ dc_username = os.environ['DC_USERNAME']
+ dc_password = os.environ['DC_PASSWORD']
+
+ username = self.randomName()
+ password = 'a'
+
+ self.addCleanup(cleanup_user, username)
+
+ # Try to add the user and ensure it fails.
+ result, out, err = self.runsubcmd('user', 'add',
+ username, password,
+ '-H', f'ldap://{server}',
+ f'-U{dc_username}%{dc_password}')
+ self.assertCmdFail(result)
+ self.assertIn('Failed to add user', err)
+ self.assertIn('LDAP_CONSTRAINT_VIOLATION', err)
+ self.assertIn(f'{werror.WERR_PASSWORD_RESTRICTION:08X}', err)
+
+ # Now search for the user, and make sure we don't find anything.
+ res = self.samdb.search(self.samdb.domain_dn(),
+ expression=f'(sAMAccountName={username})',
+ scope=ldb.SCOPE_SUBTREE)
+ self.assertEqual(0, len(res), 'expected not to find the user')
+
+ def _verify_supplementalCredentials(self, ldif,
+ min_packages=3,
+ max_packages=6):
+ msgs = self.samdb.parse_ldif(ldif)
+ (changetype, obj) = next(msgs)
+
+ self.assertIn("supplementalCredentials", obj, "supplementalCredentials attribute required")
+ sc_blob = obj["supplementalCredentials"][0]
+ sc = ndr_unpack(drsblobs.supplementalCredentialsBlob, sc_blob)
+
+ self.assertGreaterEqual(sc.sub.num_packages,
+ min_packages, "min_packages check")
+ self.assertLessEqual(sc.sub.num_packages,
+ max_packages, "max_packages check")
+
+ if max_packages == 0:
+ return
+
+ def find_package(packages, name, start_idx=0):
+ for i in range(start_idx, len(packages)):
+ if packages[i].name == name:
+ return (i, packages[i])
+ return (None, None)
+
+ # The ordering is this
+ #
+ # Primary:Kerberos-Newer-Keys (optional)
+ # Primary:Kerberos
+ # Primary:WDigest
+ # Primary:CLEARTEXT (optional)
+ # Primary:SambaGPG (optional)
+ #
+ # And the 'Packages' package is insert before the last
+ # other package.
+
+ nidx = 0
+ (pidx, pp) = find_package(sc.sub.packages, "Packages", start_idx=nidx)
+ self.assertIsNotNone(pp, "Packages required")
+ self.assertEqual(pidx + 1, sc.sub.num_packages - 1,
+ "Packages needs to be at num_packages - 1")
+
+ (knidx, knp) = find_package(sc.sub.packages, "Primary:Kerberos-Newer-Keys",
+ start_idx=nidx)
+ if knidx is not None:
+ self.assertEqual(knidx, nidx, "Primary:Kerberos-Newer-Keys at wrong position")
+ nidx = nidx + 1
+ if nidx == pidx:
+ nidx = nidx + 1
+
+ (kidx, kp) = find_package(sc.sub.packages, "Primary:Kerberos",
+ start_idx=nidx)
+ self.assertIsNotNone(pp, "Primary:Kerberos required")
+ self.assertEqual(kidx, nidx, "Primary:Kerberos at wrong position")
+ nidx = nidx + 1
+ if nidx == pidx:
+ nidx = nidx + 1
+
+ (widx, wp) = find_package(sc.sub.packages, "Primary:WDigest",
+ start_idx=nidx)
+ self.assertIsNotNone(pp, "Primary:WDigest required")
+ self.assertEqual(widx, nidx, "Primary:WDigest at wrong position")
+ nidx = nidx + 1
+ if nidx == pidx:
+ nidx = nidx + 1
+
+ (cidx, cp) = find_package(sc.sub.packages, "Primary:CLEARTEXT",
+ start_idx=nidx)
+ if cidx is not None:
+ self.assertEqual(cidx, nidx, "Primary:CLEARTEXT at wrong position")
+ nidx = nidx + 1
+ if nidx == pidx:
+ nidx = nidx + 1
+
+ (gidx, gp) = find_package(sc.sub.packages, "Primary:SambaGPG",
+ start_idx=nidx)
+ if gidx is not None:
+ self.assertEqual(gidx, nidx, "Primary:SambaGPG at wrong position")
+ nidx = nidx + 1
+ if nidx == pidx:
+ nidx = nidx + 1
+
+ self.assertEqual(nidx, sc.sub.num_packages, "Unknown packages found")
+
+ def test_setpassword(self):
+ expect_nt_hash = bool(int(os.environ.get("EXPECT_NT_HASH", "1")))
+
+ for user in self.users:
+ newpasswd = self.random_password(16)
+ (result, out, err) = self.runsubcmd("user", "setpassword",
+ user["name"],
+ "--newpassword=%s" % newpasswd,
+ "-H", "ldap://%s" % os.environ["DC_SERVER"],
+ "-U%s%%%s" % (os.environ["DC_USERNAME"], os.environ["DC_PASSWORD"]))
+ self.assertCmdSuccess(result, out, err, "Ensure setpassword runs")
+ self.assertEqual(err, "", "setpassword with url")
+ self.assertMatch(out, "Changed password OK", "setpassword with url")
+
+ attributes = "sAMAccountName,unicodePwd,supplementalCredentials,virtualClearTextUTF8,virtualClearTextUTF16,virtualSSHA,virtualSambaGPG"
+ (result, out, err) = self.runsubcmd("user", "syncpasswords",
+ "--cache-ldb-initialize",
+ "--attributes=%s" % attributes,
+ "--decrypt-samba-gpg")
+ self.assertCmdSuccess(result, out, err, "Ensure syncpasswords --cache-ldb-initialize runs")
+ self.assertEqual(err, "", "getpassword without url")
+ cache_attrs = {
+ "objectClass": {"value": "userSyncPasswords"},
+ "samdbUrl": {},
+ "dirsyncFilter": {},
+ "dirsyncAttribute": {},
+ "dirsyncControl": {"value": "dirsync:1:0:0"},
+ "passwordAttribute": {},
+ "decryptSambaGPG": {},
+ "currentTime": {},
+ }
+ for a in cache_attrs.keys():
+ v = cache_attrs[a].get("value", "")
+ self.assertMatch(out, "%s: %s" % (a, v),
+ "syncpasswords --cache-ldb-initialize: %s: %s out[%s]" % (a, v, out))
+
+ (result, out, err) = self.runsubcmd("user", "syncpasswords", "--no-wait")
+ self.assertCmdSuccess(result, out, err, "Ensure syncpasswords --no-wait runs")
+ self.assertEqual(err, "", "syncpasswords --no-wait")
+ self.assertMatch(out, "dirsync_loop(): results 0",
+ "syncpasswords --no-wait: 'dirsync_loop(): results 0': out[%s]" % (out))
+ for user in self.users:
+ self.assertMatch(out, "sAMAccountName: %s" % (user["name"]),
+ "syncpasswords --no-wait: 'sAMAccountName': %s out[%s]" % (user["name"], out))
+
+ for user in self.users:
+ newpasswd = self.random_password(16)
+ creds = credentials.Credentials()
+ creds.set_anonymous()
+ creds.set_password(newpasswd)
+ unicodePwd = base64.b64encode(creds.get_nt_hash()).decode('utf8')
+ virtualClearTextUTF8 = base64.b64encode(get_bytes(newpasswd)).decode('utf8')
+ virtualClearTextUTF16 = base64.b64encode(get_string(newpasswd).encode('utf-16-le')).decode('utf8')
+
+ (result, out, err) = self.runsubcmd("user", "setpassword",
+ user["name"],
+ "--newpassword=%s" % newpasswd)
+ self.assertCmdSuccess(result, out, err, "Ensure setpassword runs")
+ self.assertEqual(err, "", "setpassword without url")
+ self.assertMatch(out, "Changed password OK", "setpassword without url")
+
+ (result, out, err) = self.runsubcmd("user", "syncpasswords", "--no-wait")
+ self.assertCmdSuccess(result, out, err, "Ensure syncpasswords --no-wait runs")
+ self.assertEqual(err, "", "syncpasswords --no-wait")
+ self.assertMatch(out, "dirsync_loop(): results 0",
+ "syncpasswords --no-wait: 'dirsync_loop(): results 0': out[%s]" % (out))
+ self.assertMatch(out, "sAMAccountName: %s" % (user["name"]),
+ "syncpasswords --no-wait: 'sAMAccountName': %s out[%s]" % (user["name"], out))
+ self.assertMatch(out, "# unicodePwd::: REDACTED SECRET ATTRIBUTE",
+ "getpassword '# unicodePwd::: REDACTED SECRET ATTRIBUTE': out[%s]" % out)
+ if expect_nt_hash or "virtualSambaGPG:: " in out:
+ self.assertMatch(out, "unicodePwd:: %s" % unicodePwd,
+ "getpassword unicodePwd: out[%s]" % out)
+ else:
+ self.assertNotIn("unicodePwd:: %s" % unicodePwd, out)
+ self.assertMatch(out, "# supplementalCredentials::: REDACTED SECRET ATTRIBUTE",
+ "getpassword '# supplementalCredentials::: REDACTED SECRET ATTRIBUTE': out[%s]" % out)
+ self.assertMatch(out, "supplementalCredentials:: ",
+ "getpassword supplementalCredentials: out[%s]" % out)
+ if "virtualSambaGPG:: " in out:
+ self.assertMatch(out, "virtualClearTextUTF8:: %s" % virtualClearTextUTF8,
+ "getpassword virtualClearTextUTF8: out[%s]" % out)
+ self.assertMatch(out, "virtualClearTextUTF16:: %s" % virtualClearTextUTF16,
+ "getpassword virtualClearTextUTF16: out[%s]" % out)
+ self.assertMatch(out, "virtualSSHA: ",
+ "getpassword virtualSSHA: out[%s]" % out)
+
+ (result, out, err) = self.runsubcmd("user", "getpassword",
+ user["name"],
+ "--attributes=%s" % attributes,
+ "--decrypt-samba-gpg")
+ self.assertCmdSuccess(result, out, err, "Ensure getpassword runs")
+ self.assertEqual(err, "Got password OK\n", "getpassword without url")
+ self.assertMatch(out, "sAMAccountName: %s" % (user["name"]),
+ "getpassword: 'sAMAccountName': %s out[%s]" % (user["name"], out))
+ if expect_nt_hash or "virtualSambaGPG:: " in out:
+ self.assertMatch(out, "unicodePwd:: %s" % unicodePwd,
+ "getpassword unicodePwd: out[%s]" % out)
+ else:
+ self.assertNotIn("unicodePwd:: %s" % unicodePwd, out)
+ self.assertMatch(out, "supplementalCredentials:: ",
+ "getpassword supplementalCredentials: out[%s]" % out)
+ self._verify_supplementalCredentials(out)
+ if "virtualSambaGPG:: " in out:
+ self.assertMatch(out, "virtualClearTextUTF8:: %s" % virtualClearTextUTF8,
+ "getpassword virtualClearTextUTF8: out[%s]" % out)
+ self.assertMatch(out, "virtualClearTextUTF16:: %s" % virtualClearTextUTF16,
+ "getpassword virtualClearTextUTF16: out[%s]" % out)
+ self.assertMatch(out, "virtualSSHA: ",
+ "getpassword virtualSSHA: out[%s]" % out)
+
+ for user in self.users:
+ newpasswd = self.random_password(16)
+ (result, out, err) = self.runsubcmd("user", "setpassword",
+ user["name"],
+ "--newpassword=%s" % newpasswd,
+ "--must-change-at-next-login",
+ "-H", "ldap://%s" % os.environ["DC_SERVER"],
+ "-U%s%%%s" % (os.environ["DC_USERNAME"], os.environ["DC_PASSWORD"]))
+ self.assertCmdSuccess(result, out, err, "Ensure setpassword runs")
+ self.assertEqual(err, "", "setpassword with forced change")
+ self.assertMatch(out, "Changed password OK", "setpassword with forced change")
+
+ def test_setexpiry(self):
+ for user in self.users:
+ twodays = time.time() + (2 * 24 * 60 * 60)
+
+ (result, out, err) = self.runsubcmd("user", "setexpiry", user["name"],
+ "--days=2",
+ "-H", "ldap://%s" % os.environ["DC_SERVER"],
+ "-U%s%%%s" % (os.environ["DC_USERNAME"], os.environ["DC_PASSWORD"]))
+ self.assertCmdSuccess(result, out, err, "Can we run setexpiry with names")
+ self.assertIn("Expiry for user '%s' set to 2 days." % user["name"], out)
+
+ found = self._find_user(user["name"])
+
+ expires = nttime2unix(int("%s" % found.get("accountExpires")))
+ self.assertWithin(expires, twodays, 5, "Ensure account expires is within 5 seconds of the expected time")
+
+ # TODO: re-enable this after the filter case is sorted out
+ if "filters are broken, bail now":
+ return
+
+ # now run the expiration based on a filter
+ fourdays = time.time() + (4 * 24 * 60 * 60)
+ (result, out, err) = self.runsubcmd("user", "setexpiry",
+ "--filter", "(&(objectClass=user)(company=comp2))",
+ "--days=4",
+ "-H", "ldap://%s" % os.environ["DC_SERVER"],
+ "-U%s%%%s" % (os.environ["DC_USERNAME"], os.environ["DC_PASSWORD"]))
+ self.assertCmdSuccess(result, out, err, "Can we run setexpiry with a filter")
+
+ for user in self.users:
+ found = self._find_user(user["name"])
+ if ("%s" % found.get("company")) == "comp2":
+ expires = nttime2unix(int("%s" % found.get("accountExpires")))
+ self.assertWithin(expires, fourdays, 5, "Ensure account expires is within 5 seconds of the expected time")
+ else:
+ expires = nttime2unix(int("%s" % found.get("accountExpires")))
+ self.assertWithin(expires, twodays, 5, "Ensure account expires is within 5 seconds of the expected time")
+
+ def test_list(self):
+ (result, out, err) = self.runsubcmd("user", "list",
+ "-H", "ldap://%s" % os.environ["DC_SERVER"],
+ "-U%s%%%s" % (os.environ["DC_USERNAME"],
+ os.environ["DC_PASSWORD"]))
+ self.assertCmdSuccess(result, out, err, "Error running list")
+
+ search_filter = ("(&(objectClass=user)(userAccountControl:%s:=%u))" %
+ (ldb.OID_COMPARATOR_AND, dsdb.UF_NORMAL_ACCOUNT))
+
+ userlist = self.samdb.search(base=self.samdb.domain_dn(),
+ scope=ldb.SCOPE_SUBTREE,
+ expression=search_filter,
+ attrs=["samaccountname"])
+
+ self.assertTrue(len(userlist) > 0, "no users found in samdb")
+
+ for userobj in userlist:
+ name = str(userobj.get("samaccountname", idx=0))
+ self.assertMatch(out, name,
+ "user '%s' not found" % name)
+
+
+ def test_list_base_dn(self):
+ base_dn = "CN=Users"
+ (result, out, err) = self.runsubcmd("user", "list", "-b", base_dn,
+ "-H", "ldap://%s" % os.environ["DC_SERVER"],
+ "-U%s%%%s" % (os.environ["DC_USERNAME"],
+ os.environ["DC_PASSWORD"]))
+ self.assertCmdSuccess(result, out, err, "Error running list")
+
+ search_filter = ("(&(objectClass=user)(userAccountControl:%s:=%u))" %
+ (ldb.OID_COMPARATOR_AND, dsdb.UF_NORMAL_ACCOUNT))
+
+ userlist = self.samdb.search(base=self.samdb.normalize_dn_in_domain(base_dn),
+ scope=ldb.SCOPE_SUBTREE,
+ expression=search_filter,
+ attrs=["samaccountname"])
+
+ self.assertTrue(len(userlist) > 0, "no users found in samdb")
+
+ for userobj in userlist:
+ name = str(userobj.get("samaccountname", idx=0))
+ self.assertMatch(out, name,
+ "user '%s' not found" % name)
+
+ def test_list_full_dn(self):
+ (result, out, err) = self.runsubcmd("user", "list", "--full-dn",
+ "-H", "ldap://%s" % os.environ["DC_SERVER"],
+ "-U%s%%%s" % (os.environ["DC_USERNAME"],
+ os.environ["DC_PASSWORD"]))
+ self.assertCmdSuccess(result, out, err, "Error running list")
+
+ search_filter = ("(&(objectClass=user)(userAccountControl:%s:=%u))" %
+ (ldb.OID_COMPARATOR_AND, dsdb.UF_NORMAL_ACCOUNT))
+
+ userlist = self.samdb.search(base=self.samdb.domain_dn(),
+ scope=ldb.SCOPE_SUBTREE,
+ expression=search_filter,
+ attrs=["dn"])
+
+ self.assertTrue(len(userlist) > 0, "no users found in samdb")
+
+ for userobj in userlist:
+ name = str(userobj.get("dn", idx=0))
+ self.assertMatch(out, name,
+ "user '%s' not found" % name)
+
+ def test_list_hide_expired(self):
+ expire_username = "expireUser"
+ expire_user = self._randomUser({"name": expire_username})
+ self._create_user(expire_user)
+
+ (result, out, err) = self.runsubcmd(
+ "user",
+ "list",
+ "--hide-expired",
+ "-H",
+ "ldap://%s" % os.environ["DC_SERVER"],
+ "-U%s%%%s" % (os.environ["DC_USERNAME"],
+ os.environ["DC_PASSWORD"]))
+ self.assertCmdSuccess(result, out, err, "Error running list")
+ self.assertTrue(expire_username in out,
+ "user '%s' not found" % expire_username)
+
+ # user will be expired one second ago
+ self.samdb.setexpiry(
+ "(sAMAccountname=%s)" % expire_username,
+ -1,
+ False)
+
+ (result, out, err) = self.runsubcmd(
+ "user",
+ "list",
+ "--hide-expired",
+ "-H",
+ "ldap://%s" % os.environ["DC_SERVER"],
+ "-U%s%%%s" % (os.environ["DC_USERNAME"],
+ os.environ["DC_PASSWORD"]))
+ self.assertCmdSuccess(result, out, err, "Error running list")
+ self.assertFalse(expire_username in out,
+ "user '%s' found" % expire_username)
+
+ self.samdb.deleteuser(expire_username)
+
+ def test_list_hide_disabled(self):
+ disable_username = "disableUser"
+ disable_user = self._randomUser({"name": disable_username})
+ self._create_user(disable_user)
+
+ (result, out, err) = self.runsubcmd(
+ "user",
+ "list",
+ "--hide-disabled",
+ "-H",
+ "ldap://%s" % os.environ["DC_SERVER"],
+ "-U%s%%%s" % (os.environ["DC_USERNAME"],
+ os.environ["DC_PASSWORD"]))
+ self.assertCmdSuccess(result, out, err, "Error running list")
+ self.assertTrue(disable_username in out,
+ "user '%s' not found" % disable_username)
+
+ self.samdb.disable_account("(sAMAccountname=%s)" % disable_username)
+
+ (result, out, err) = self.runsubcmd(
+ "user",
+ "list",
+ "--hide-disabled",
+ "-H",
+ "ldap://%s" % os.environ["DC_SERVER"],
+ "-U%s%%%s" % (os.environ["DC_USERNAME"],
+ os.environ["DC_PASSWORD"]))
+ self.assertCmdSuccess(result, out, err, "Error running list")
+ self.assertFalse(disable_username in out,
+ "user '%s' found" % disable_username)
+
+ self.samdb.deleteuser(disable_username)
+
+ def test_show(self):
+ for user in self.users:
+ (result, out, err) = self.runsubcmd(
+ "user", "show", user["name"],
+ "--attributes=sAMAccountName,company",
+ "-H", "ldap://%s" % os.environ["DC_SERVER"],
+ "-U%s%%%s" % (os.environ["DC_USERNAME"],
+ os.environ["DC_PASSWORD"]))
+ self.assertCmdSuccess(result, out, err, "Error running show")
+
+ expected_out = """dn: CN=%s %s,CN=Users,%s
+company: %s
+sAMAccountName: %s
+
+""" % (user["given-name"], user["surname"], self.samdb.domain_dn(),
+ user["company"], user["name"])
+
+ self.assertEqual(out, expected_out,
+ "Unexpected show output for user '%s'" %
+ user["name"])
+
+ time_attrs = [
+ "name", # test that invalid values are just ignored
+ "whenCreated",
+ "whenChanged",
+ "accountExpires",
+ "badPasswordTime",
+ "lastLogoff",
+ "lastLogon",
+ "lastLogonTimestamp",
+ "lockoutTime",
+ "msDS-UserPasswordExpiryTimeComputed",
+ "pwdLastSet",
+ ]
+
+ attrs = []
+ for ta in time_attrs:
+ attrs.append(ta)
+ for fm in ["GeneralizedTime", "UnixTime", "TimeSpec"]:
+ attrs.append("%s;format=%s" % (ta, fm))
+
+ (result, out, err) = self.runsubcmd(
+ "user", "show", user["name"],
+ "--attributes=%s" % ",".join(attrs),
+ "-H", "ldap://%s" % os.environ["DC_SERVER"],
+ "-U%s%%%s" % (os.environ["DC_USERNAME"],
+ os.environ["DC_PASSWORD"]))
+ self.assertCmdSuccess(result, out, err, "Error running show")
+
+ self.assertIn(";format=GeneralizedTime", out)
+ self.assertIn(";format=UnixTime", out)
+ self.assertIn(";format=TimeSpec", out)
+
+ self.assertIn("name: ", out)
+ self.assertNotIn("name;format=GeneralizedTime: ", out)
+ self.assertNotIn("name;format=UnixTime: ", out)
+ self.assertNotIn("name;format=TimeSpec: ", out)
+
+ self.assertIn("whenCreated: 20", out)
+ self.assertIn("whenCreated;format=GeneralizedTime: 20", out)
+ self.assertIn("whenCreated;format=UnixTime: 1", out)
+ self.assertIn("whenCreated;format=TimeSpec: 1", out)
+
+ self.assertIn("whenChanged: 20", out)
+ self.assertIn("whenChanged;format=GeneralizedTime: 20", out)
+ self.assertIn("whenChanged;format=UnixTime: 1", out)
+ self.assertIn("whenChanged;format=TimeSpec: 1", out)
+
+ self.assertIn("accountExpires: 9223372036854775807", out)
+ self.assertNotIn("accountExpires;format=GeneralizedTime: ", out)
+ self.assertNotIn("accountExpires;format=UnixTime: ", out)
+ self.assertNotIn("accountExpires;format=TimeSpec: ", out)
+
+ self.assertIn("badPasswordTime: 0", out)
+ self.assertNotIn("badPasswordTime;format=GeneralizedTime: ", out)
+ self.assertNotIn("badPasswordTime;format=UnixTime: ", out)
+ self.assertNotIn("badPasswordTime;format=TimeSpec: ", out)
+
+ self.assertIn("lastLogoff: 0", out)
+ self.assertNotIn("lastLogoff;format=GeneralizedTime: ", out)
+ self.assertNotIn("lastLogoff;format=UnixTime: ", out)
+ self.assertNotIn("lastLogoff;format=TimeSpec: ", out)
+
+ self.assertIn("lastLogon: 0", out)
+ self.assertNotIn("lastLogon;format=GeneralizedTime: ", out)
+ self.assertNotIn("lastLogon;format=UnixTime: ", out)
+ self.assertNotIn("lastLogon;format=TimeSpec: ", out)
+
+ # If a specified attribute is not available on a user object
+ # it's silently omitted.
+ self.assertNotIn("lastLogonTimestamp:", out)
+ self.assertNotIn("lockoutTime:", out)
+
+ self.assertIn("msDS-UserPasswordExpiryTimeComputed: 1", out)
+ self.assertIn("msDS-UserPasswordExpiryTimeComputed;format=GeneralizedTime: 20", out)
+ self.assertIn("msDS-UserPasswordExpiryTimeComputed;format=UnixTime: 1", out)
+ self.assertIn("msDS-UserPasswordExpiryTimeComputed;format=TimeSpec: 1", out)
+
+ self.assertIn("pwdLastSet: 1", out)
+ self.assertIn("pwdLastSet;format=GeneralizedTime: 20", out)
+ self.assertIn("pwdLastSet;format=UnixTime: 1", out)
+ self.assertIn("pwdLastSet;format=TimeSpec: 1", out)
+
+ out_msgs = self.samdb.parse_ldif(out)
+ out_msg = next(out_msgs)[1]
+
+ self.assertIn("whenCreated", out_msg)
+ when_created_str = str(out_msg["whenCreated"][0])
+ self.assertIn("whenCreated;format=GeneralizedTime", out_msg)
+ self.assertEqual(str(out_msg["whenCreated;format=GeneralizedTime"][0]), when_created_str)
+ when_created_time = ldb.string_to_time(when_created_str)
+ self.assertIn("whenCreated;format=UnixTime", out_msg)
+ self.assertEqual(str(out_msg["whenCreated;format=UnixTime"][0]), str(when_created_time))
+ self.assertIn("whenCreated;format=TimeSpec", out_msg)
+ self.assertEqual(str(out_msg["whenCreated;format=TimeSpec"][0]),
+ "%d.000000000" % (when_created_time))
+
+ self.assertIn("whenChanged", out_msg)
+ when_changed_str = str(out_msg["whenChanged"][0])
+ self.assertIn("whenChanged;format=GeneralizedTime", out_msg)
+ self.assertEqual(str(out_msg["whenChanged;format=GeneralizedTime"][0]), when_changed_str)
+ when_changed_time = ldb.string_to_time(when_changed_str)
+ self.assertIn("whenChanged;format=UnixTime", out_msg)
+ self.assertEqual(str(out_msg["whenChanged;format=UnixTime"][0]), str(when_changed_time))
+ self.assertIn("whenChanged;format=TimeSpec", out_msg)
+ self.assertEqual(str(out_msg["whenChanged;format=TimeSpec"][0]),
+ "%d.000000000" % (when_changed_time))
+
+ self.assertIn("pwdLastSet;format=GeneralizedTime", out_msg)
+ pwd_last_set_str = str(out_msg["pwdLastSet;format=GeneralizedTime"][0])
+ pwd_last_set_time = ldb.string_to_time(pwd_last_set_str)
+ self.assertIn("pwdLastSet;format=UnixTime", out_msg)
+ self.assertEqual(str(out_msg["pwdLastSet;format=UnixTime"][0]), str(pwd_last_set_time))
+ self.assertIn("pwdLastSet;format=TimeSpec", out_msg)
+ self.assertIn("%d." % pwd_last_set_time, str(out_msg["pwdLastSet;format=TimeSpec"][0]))
+ self.assertNotIn(".000000000", str(out_msg["pwdLastSet;format=TimeSpec"][0]))
+
+ # assert that the pwd has been set in the minute after user creation
+ self.assertGreaterEqual(pwd_last_set_time, when_created_time)
+ self.assertLess(pwd_last_set_time, when_created_time + 60)
+
+ self.assertIn("msDS-UserPasswordExpiryTimeComputed;format=GeneralizedTime", out_msg)
+ pwd_expires_str = str(out_msg["msDS-UserPasswordExpiryTimeComputed;format=GeneralizedTime"][0])
+ pwd_expires_time = ldb.string_to_time(pwd_expires_str)
+ self.assertIn("msDS-UserPasswordExpiryTimeComputed;format=UnixTime", out_msg)
+ self.assertEqual(str(out_msg["msDS-UserPasswordExpiryTimeComputed;format=UnixTime"][0]), str(pwd_expires_time))
+ self.assertIn("msDS-UserPasswordExpiryTimeComputed;format=TimeSpec", out_msg)
+ self.assertIn("%d." % pwd_expires_time, str(out_msg["msDS-UserPasswordExpiryTimeComputed;format=TimeSpec"][0]))
+ self.assertNotIn(".000000000", str(out_msg["msDS-UserPasswordExpiryTimeComputed;format=TimeSpec"][0]))
+
+ # assert that the pwd expires after it was set
+ self.assertGreater(pwd_expires_time, pwd_last_set_time)
+
+ def test_move(self):
+ full_ou_dn = str(self.samdb.normalize_dn_in_domain("OU=movetest_usr"))
+ self.addCleanup(self.samdb.delete, full_ou_dn, ["tree_delete:1"])
+
+ (result, out, err) = self.runsubcmd("ou", "add", full_ou_dn)
+ self.assertCmdSuccess(result, out, err)
+ self.assertEqual(err, "", "There shouldn't be any error message")
+ self.assertIn('Added ou "%s"' % full_ou_dn, out)
+
+ for user in self.users:
+ (result, out, err) = self.runsubcmd(
+ "user", "move", user["name"], full_ou_dn)
+ self.assertCmdSuccess(result, out, err, "Error running move")
+ self.assertIn('Moved user "%s" into "%s"' %
+ (user["name"], full_ou_dn), out)
+
+ # Should fail as users objects are in OU
+ (result, out, err) = self.runsubcmd("ou", "delete", full_ou_dn)
+ self.assertCmdFail(result)
+ self.assertIn(("subtree_delete: Unable to delete a non-leaf node "
+ "(it has %d children)!") % len(self.users), err)
+
+ for user in self.users:
+ new_dn = "CN=Users,%s" % self.samdb.domain_dn()
+ (result, out, err) = self.runsubcmd(
+ "user", "move", user["name"], new_dn)
+ self.assertCmdSuccess(result, out, err, "Error running move")
+ self.assertIn('Moved user "%s" into "%s"' %
+ (user["name"], new_dn), out)
+
+ def test_rename_surname_initials_givenname(self):
+ """rename the existing surname and given name and add missing
+ initials, then remove them, for all users"""
+ for user in self.users:
+ new_givenname = "new_given_name_of_" + user["name"]
+ new_initials = "A"
+ new_surname = "new_surname_of_" + user["name"]
+ found = self._find_user(user["name"])
+ old_cn = str(found.get("cn"))
+
+ # rename given name, initials and surname
+ (result, out, err) = self.runsubcmd("user", "rename", user["name"],
+ "--surname=%s" % new_surname,
+ "--initials=%s" % new_initials,
+ "--given-name=%s" % new_givenname)
+ self.assertCmdSuccess(result, out, err)
+ self.assertEqual(err, "", "Shouldn't be any error messages")
+ self.assertIn('successfully', out)
+
+ found = self._find_user(user["name"])
+ self.assertEqual("%s" % found.get("givenName"), new_givenname)
+ self.assertEqual("%s" % found.get("initials"), new_initials)
+ self.assertEqual("%s" % found.get("sn"), new_surname)
+ self.assertEqual("%s" % found.get("name"),
+ "%s %s. %s" % (new_givenname, new_initials, new_surname))
+ self.assertEqual("%s" % found.get("cn"),
+ "%s %s. %s" % (new_givenname, new_initials, new_surname))
+
+ # remove given name, initials and surname
+ (result, out, err) = self.runsubcmd("user", "rename", user["name"],
+ "--surname=",
+ "--initials=",
+ "--given-name=")
+ self.assertCmdSuccess(result, out, err)
+ self.assertEqual(err, "", "Shouldn't be any error messages")
+ self.assertIn('successfully', out)
+
+ found = self._find_user(user["name"])
+ self.assertEqual(found.get("givenName"), None)
+ self.assertEqual(found.get("initials"), None)
+ self.assertEqual(found.get("sn"), None)
+ self.assertEqual("%s" % found.get("cn"), user["name"])
+
+ # reset changes (initials are removed)
+ (result, out, err) = self.runsubcmd("user", "rename", user["name"],
+ "--surname=%(surname)s" % user,
+ "--given-name=%(given-name)s" % user)
+ self.assertCmdSuccess(result, out, err)
+
+ if old_cn:
+ (result, out, err) = self.runsubcmd("user", "rename", user["name"],
+ "--force-new-cn=%s" % old_cn)
+
+ def test_rename_cn_samaccountname(self):
+ """rename and try to remove the cn and the samaccount of all users"""
+ for user in self.users:
+ new_cn = "new_cn_of_" + user["name"]
+ new_samaccountname = "new_samaccount_of_" + user["name"]
+ new_surname = "new_surname_of_" + user["name"]
+
+ # rename cn
+ (result, out, err) = self.runsubcmd("user", "rename", user["name"],
+ "--samaccountname=%s"
+ % new_samaccountname,
+ "--force-new-cn=%s" % new_cn)
+ self.assertCmdSuccess(result, out, err)
+ self.assertEqual(err, "", "Shouldn't be any error messages")
+ self.assertIn('successfully', out)
+
+ found = self._find_user(new_samaccountname)
+ self.assertEqual("%s" % found.get("cn"), new_cn)
+ self.assertEqual("%s" % found.get("sAMAccountName"),
+ new_samaccountname)
+
+ # changing the surname has no effect to the cn
+ (result, out, err) = self.runsubcmd("user", "rename", new_samaccountname,
+ "--surname=%s" % new_surname)
+ self.assertCmdSuccess(result, out, err)
+
+ found = self._find_user(new_samaccountname)
+ self.assertEqual("%s" % found.get("cn"), new_cn)
+
+ # trying to remove cn (throws an error)
+ (result, out, err) = self.runsubcmd("user", "rename",
+ new_samaccountname,
+ "--force-new-cn=")
+ self.assertCmdFail(result)
+ self.assertIn('Failed to rename user', err)
+ self.assertIn("delete protected attribute", err)
+
+ # trying to remove the samccountname (throws an error)
+ (result, out, err) = self.runsubcmd("user", "rename",
+ new_samaccountname,
+ "--samaccountname=")
+ self.assertCmdFail(result)
+ self.assertIn('Failed to rename user', err)
+ self.assertIn('delete protected attribute', err)
+
+ # reset changes (cn must be the name)
+ (result, out, err) = self.runsubcmd("user", "rename", new_samaccountname,
+ "--samaccountname=%(name)s"
+ % user,
+ "--force-new-cn=%(name)s" % user)
+ self.assertCmdSuccess(result, out, err)
+
+ def test_rename_standard_cn(self):
+ """reset the cn of all users to the standard"""
+ for user in self.users:
+ new_cn = "new_cn_of_" + user["name"]
+ new_givenname = "new_given_name_of_" + user["name"]
+ new_initials = "A"
+ new_surname = "new_surname_of_" + user["name"]
+
+ # set different cn
+ (result, out, err) = self.runsubcmd("user", "rename", user["name"],
+ "--force-new-cn=%s" % new_cn)
+ self.assertCmdSuccess(result, out, err)
+
+ # remove given name, initials and surname
+ (result, out, err) = self.runsubcmd("user", "rename", user["name"],
+ "--surname=",
+ "--initials=",
+ "--given-name=")
+ self.assertCmdSuccess(result, out, err)
+
+ # reset the CN (no given name, initials or surname --> samaccountname)
+ (result, out, err) = self.runsubcmd("user", "rename", user["name"],
+ "--reset-cn")
+
+ self.assertCmdSuccess(result, out, err)
+ self.assertEqual(err, "", "Shouldn't be any error messages")
+ self.assertIn('successfully', out)
+
+ found = self._find_user(user["name"])
+ self.assertEqual("%s" % found.get("cn"), user["name"])
+
+ # set given name, initials and surname and set different cn
+ (result, out, err) = self.runsubcmd("user", "rename", user["name"],
+ "--force-new-cn=%s" % new_cn,
+ "--surname=%s" % new_surname,
+ "--initials=%s" % new_initials,
+ "--given-name=%s" % new_givenname)
+ self.assertCmdSuccess(result, out, err)
+
+ # reset the CN (given name, initials or surname are given --> given name)
+ (result, out, err) = self.runsubcmd("user", "rename", user["name"],
+ "--reset-cn")
+
+ self.assertCmdSuccess(result, out, err)
+ self.assertEqual(err, "", "Shouldn't be any error messages")
+ self.assertIn('successfully', out)
+
+ found = self._find_user(user["name"])
+ self.assertEqual("%s" % found.get("cn"),
+ "%s %s. %s" % (new_givenname, new_initials, new_surname))
+
+ # reset changes
+ (result, out, err) = self.runsubcmd("user", "rename", user["name"],
+ "--reset-cn",
+ "--initials=",
+ "--surname=%(surname)s" % user,
+ "--given-name=%(given-name)s" % user)
+ self.assertCmdSuccess(result, out, err)
+
+ def test_rename_mailaddress_displayname(self):
+ for user in self.users:
+ new_mail = "new_mailaddress_of_" + user["name"]
+ new_displayname = "new displayname of " + user["name"]
+
+ # change mail and displayname
+ (result, out, err) = self.runsubcmd("user", "rename", user["name"],
+ "--mail-address=%s"
+ % new_mail,
+ "--display-name=%s"
+ % new_displayname)
+ self.assertCmdSuccess(result, out, err)
+ self.assertEqual(err, "", "Shouldn't be any error messages")
+ self.assertIn('successfully', out)
+
+ found = self._find_user(user["name"])
+ self.assertEqual("%s" % found.get("mail"), new_mail)
+ self.assertEqual("%s" % found.get("displayName"), new_displayname)
+
+ # remove mail and displayname
+ (result, out, err) = self.runsubcmd("user", "rename", user["name"],
+ "--mail-address=",
+ "--display-name=")
+ self.assertCmdSuccess(result, out, err)
+ self.assertEqual(err, "", "Shouldn't be any error messages")
+ self.assertIn('successfully', out)
+
+ found = self._find_user(user["name"])
+ self.assertEqual(found.get("mail"), None)
+ self.assertEqual(found.get("displayName"), None)
+
+ def test_rename_upn(self):
+ """rename upn of all users"""
+ for user in self.users:
+ found = self._find_user(user["name"])
+ old_upn = "%s" % found.get("userPrincipalName")
+ valid_suffix = old_upn.split('@')[1] # samba.example.com
+
+ valid_new_upn = "new_%s@%s" % (user["name"], valid_suffix)
+ invalid_new_upn = "%s@invalid.suffix" + user["name"]
+
+ # trying to set invalid upn
+ (result, out, err) = self.runsubcmd("user", "rename", user["name"],
+ "--upn=%s"
+ % invalid_new_upn)
+ self.assertCmdFail(result)
+ self.assertIn('is not a valid upn', err)
+
+ # set valid upn
+ (result, out, err) = self.runsubcmd("user", "rename", user["name"],
+ "--upn=%s"
+ % valid_new_upn)
+ self.assertCmdSuccess(result, out, err)
+ self.assertEqual(err, "", "Shouldn't be any error messages")
+ self.assertIn('successfully', out)
+
+ found = self._find_user(user["name"])
+ self.assertEqual("%s" % found.get("userPrincipalName"), valid_new_upn)
+
+ # trying to remove upn
+ (result, out, err) = self.runsubcmd("user", "rename", user["name"],
+ "--upn=%s")
+ self.assertCmdFail(result)
+ self.assertIn('is not a valid upn', err)
+
+ # reset upn
+ (result, out, err) = self.runsubcmd("user", "rename", user["name"],
+ "--upn=%s" % old_upn)
+ self.assertCmdSuccess(result, out, err)
+
+ def test_getpwent(self):
+ try:
+ import pwd
+ except ImportError:
+ self.skipTest("Skipping getpwent test, no 'pwd' module available")
+ return
+
+ # get the current user's data for the test
+ uid = os.geteuid()
+ try:
+ u = pwd.getpwuid(uid)
+ except KeyError:
+ self.skipTest("Skipping getpwent test, current EUID not found in NSS")
+ return
+
+
+# samba-tool user create command didn't support users with empty gecos if none is
+# specified on the command line and the user hasn't one in the passwd file it
+# will fail, so let's add some contents
+
+ gecos = u[4]
+ if (gecos is None or len(gecos) == 0):
+ gecos = "Foo GECOS"
+ user = self._randomPosixUser({
+ "name": u[0],
+ "uid": u[0],
+ "uidNumber": u[2],
+ "gidNumber": u[3],
+ "gecos": gecos,
+ "loginShell": u[6],
+ })
+
+ # Remove user if it already exists
+ if self._find_user(u[0]):
+ self.runsubcmd("user", "delete", u[0])
+ # check if --rfc2307-from-nss sets the same values as we got from pwd.getpwuid()
+ (result, out, err) = self.runsubcmd("user", "create", user["name"], user["password"],
+ "--surname=%s" % user["surname"],
+ "--given-name=%s" % user["given-name"],
+ "--job-title=%s" % user["job-title"],
+ "--department=%s" % user["department"],
+ "--description=%s" % user["description"],
+ "--company=%s" % user["company"],
+ "--gecos=%s" % user["gecos"],
+ "--rfc2307-from-nss",
+ "-H", "ldap://%s" % os.environ["DC_SERVER"],
+ "-U%s%%%s" % (os.environ["DC_USERNAME"], os.environ["DC_PASSWORD"]))
+
+ self.assertCmdSuccess(result, out, err)
+ self.assertEqual(err, "", "Shouldn't be any error messages")
+ self.assertIn("User '%s' added successfully" % user["name"], out)
+
+ self._check_posix_user(user)
+ self.runsubcmd("user", "delete", user["name"])
+
+ # Check if overriding the attributes from NSS with explicit values works
+ #
+ # get a user with all random posix attributes
+ user = self._randomPosixUser({"name": u[0]})
+
+ # Remove user if it already exists
+ if self._find_user(u[0]):
+ self.runsubcmd("user", "delete", u[0])
+ # create a user with posix attributes from nss but override all of them with the
+ # random ones just obtained
+ (result, out, err) = self.runsubcmd("user", "create", user["name"], user["password"],
+ "--surname=%s" % user["surname"],
+ "--given-name=%s" % user["given-name"],
+ "--job-title=%s" % user["job-title"],
+ "--department=%s" % user["department"],
+ "--description=%s" % user["description"],
+ "--company=%s" % user["company"],
+ "--rfc2307-from-nss",
+ "--gecos=%s" % user["gecos"],
+ "--login-shell=%s" % user["loginShell"],
+ "--uid=%s" % user["uid"],
+ "--uid-number=%s" % user["uidNumber"],
+ "--gid-number=%s" % user["gidNumber"],
+ "-H", "ldap://%s" % os.environ["DC_SERVER"],
+ "-U%s%%%s" % (os.environ["DC_USERNAME"], os.environ["DC_PASSWORD"]))
+
+ self.assertCmdSuccess(result, out, err)
+ self.assertEqual(err, "", "Shouldn't be any error messages")
+ self.assertIn("User '%s' added successfully" % user["name"], out)
+
+ self._check_posix_user(user)
+ self.runsubcmd("user", "delete", user["name"])
+
+ # Test: samba-tool user unlock
+ # This test does not verify that the command unlocks the user, it just
+ # tests the command itself. The unlock test, which unlocks locked users,
+ # is located in the 'samba4.ldap.password_lockout' test in
+ # source4/dsdb/tests/python/password_lockout.py
+ def test_unlock(self):
+
+ # try to unlock a nonexistent user, this should fail
+ nonexistentusername = "userdoesnotexist"
+ (result, out, err) = self.runsubcmd(
+ "user", "unlock", nonexistentusername)
+ self.assertCmdFail(result, "Ensure that unlock nonexistent user fails")
+ self.assertIn("Failed to unlock user '%s'" % nonexistentusername, err)
+ self.assertIn("Unable to find user", err)
+
+ # try to unlock with insufficient permissions, this should fail
+ unprivileged_username = "unprivilegedunlockuser"
+ unlocktest_username = "usertounlock"
+
+ self.runsubcmd("user", "add", unprivileged_username, "Passw0rd")
+ self.runsubcmd("user", "add", unlocktest_username, "Passw0rd")
+
+ (result, out, err) = self.runsubcmd(
+ "user", "unlock", unlocktest_username,
+ "-H", "ldap://%s" % os.environ["DC_SERVER"],
+ "-U%s%%%s" % (unprivileged_username,
+ "Passw0rd"))
+ self.assertCmdFail(result, "Fail with LDAP_INSUFFICIENT_ACCESS_RIGHTS")
+ self.assertIn("Failed to unlock user '%s'" % unlocktest_username, err)
+ self.assertIn("LDAP error 50 LDAP_INSUFFICIENT_ACCESS_RIGHTS", err)
+
+ self.runsubcmd("user", "delete", unprivileged_username)
+ self.runsubcmd("user", "delete", unlocktest_username)
+
+ # run unlock against test users
+ for user in self.users:
+ (result, out, err) = self.runsubcmd(
+ "user", "unlock", user["name"])
+ self.assertCmdSuccess(result, out, err, "Error running user unlock")
+ self.assertEqual(err, "", "Shouldn't be any error messages")
+
+ def _randomUser(self, base=None):
+ """create a user with random attribute values, you can specify base attributes"""
+ if base is None:
+ base = {}
+ user = {
+ "name": self.randomName(),
+ "password": self.random_password(16),
+ "surname": self.randomName(),
+ "given-name": self.randomName(),
+ "job-title": self.randomName(),
+ "department": self.randomName(),
+ "company": self.randomName(),
+ "description": self.randomName(count=100),
+ "createUserFn": self._create_user,
+ "checkUserFn": self._check_user,
+ }
+ user.update(base)
+ return user
+
+ def _randomPosixUser(self, base=None):
+ """create a user with random attribute values and additional RFC2307
+ attributes, you can specify base attributes"""
+ if base is None:
+ base = {}
+ user = self._randomUser({})
+ user.update(base)
+ posixAttributes = {
+ "uid": self.randomName(),
+ "loginShell": self.randomName(),
+ "gecos": self.randomName(),
+ "uidNumber": self.randomXid(),
+ "gidNumber": self.randomXid(),
+ "createUserFn": self._create_posix_user,
+ "checkUserFn": self._check_posix_user,
+ }
+ user.update(posixAttributes)
+ user.update(base)
+ return user
+
+ def _randomUnixUser(self, base=None):
+ """create a user with random attribute values and additional RFC2307
+ attributes, you can specify base attributes"""
+ if base is None:
+ base = {}
+ user = self._randomUser({})
+ user.update(base)
+ posixAttributes = {
+ "uidNumber": self.randomXid(),
+ "gidNumber": self.randomXid(),
+ "uid": self.randomName(),
+ "loginShell": self.randomName(),
+ "gecos": self.randomName(),
+ "createUserFn": self._create_unix_user,
+ "checkUserFn": self._check_unix_user,
+ }
+ user.update(posixAttributes)
+ user.update(base)
+ return user
+
+ def _check_user(self, user):
+ """ check if a user from SamDB has the same attributes as its template """
+ found = self._find_user(user["name"])
+
+ self.assertEqual("%s" % found.get("name"), "%(given-name)s %(surname)s" % user)
+ self.assertEqual("%s" % found.get("title"), user["job-title"])
+ self.assertEqual("%s" % found.get("company"), user["company"])
+ self.assertEqual("%s" % found.get("description"), user["description"])
+ self.assertEqual("%s" % found.get("department"), user["department"])
+
+ def _check_posix_user(self, user):
+ """ check if a posix_user from SamDB has the same attributes as its template """
+ found = self._find_user(user["name"])
+
+ self.assertEqual("%s" % found.get("loginShell"), user["loginShell"])
+ self.assertEqual("%s" % found.get("gecos"), user["gecos"])
+ self.assertEqual("%s" % found.get("uidNumber"), "%s" % user["uidNumber"])
+ self.assertEqual("%s" % found.get("gidNumber"), "%s" % user["gidNumber"])
+ self.assertEqual("%s" % found.get("uid"), user["uid"])
+ self._check_user(user)
+
+ def _check_unix_user(self, user):
+ """ check if a unix_user from SamDB has the same attributes as its
+template """
+ found = self._find_user(user["name"])
+
+ self.assertEqual("%s" % found.get("loginShell"), user["loginShell"])
+ self.assertEqual("%s" % found.get("gecos"), user["gecos"])
+ self.assertEqual("%s" % found.get("uidNumber"), "%s" %
+ user["uidNumber"])
+ self.assertEqual("%s" % found.get("gidNumber"), "%s" %
+ user["gidNumber"])
+ self.assertEqual("%s" % found.get("uid"), user["uid"])
+ self.assertIn('/home/test/', "%s" % found.get("unixHomeDirectory"))
+ self._check_user(user)
+
+ def _create_user(self, user):
+ return self.runsubcmd("user", "add", user["name"], user["password"],
+ "--surname=%s" % user["surname"],
+ "--given-name=%s" % user["given-name"],
+ "--job-title=%s" % user["job-title"],
+ "--department=%s" % user["department"],
+ "--description=%s" % user["description"],
+ "--company=%s" % user["company"],
+ "-H", "ldap://%s" % os.environ["DC_SERVER"],
+ "-U%s%%%s" % (os.environ["DC_USERNAME"], os.environ["DC_PASSWORD"]))
+
+ def _create_posix_user(self, user):
+ """ create a new user with RFC2307 attributes """
+ return self.runsubcmd("user", "create", user["name"], user["password"],
+ "--surname=%s" % user["surname"],
+ "--given-name=%s" % user["given-name"],
+ "--job-title=%s" % user["job-title"],
+ "--department=%s" % user["department"],
+ "--description=%s" % user["description"],
+ "--company=%s" % user["company"],
+ "--gecos=%s" % user["gecos"],
+ "--login-shell=%s" % user["loginShell"],
+ "--uid=%s" % user["uid"],
+ "--uid-number=%s" % user["uidNumber"],
+ "--gid-number=%s" % user["gidNumber"],
+ "-H", "ldap://%s" % os.environ["DC_SERVER"],
+ "-U%s%%%s" % (os.environ["DC_USERNAME"], os.environ["DC_PASSWORD"]))
+
+ def _create_unix_user(self, user):
+ """ Add RFC2307 attributes to a user"""
+ self._create_user(user)
+ return self.runsubcmd("user", "addunixattrs", user["name"],
+ "%s" % user["uidNumber"],
+ "--gid-number=%s" % user["gidNumber"],
+ "--gecos=%s" % user["gecos"],
+ "--login-shell=%s" % user["loginShell"],
+ "--uid=%s" % user["uid"],
+ "-H", "ldap://%s" % os.environ["DC_SERVER"],
+ "-U%s%%%s" % (os.environ["DC_USERNAME"],
+ os.environ["DC_PASSWORD"]))
+
+ def _find_user(self, name):
+ search_filter = "(&(sAMAccountName=%s)(objectCategory=%s,%s))" % (ldb.binary_encode(name), "CN=Person,CN=Schema,CN=Configuration", self.samdb.domain_dn())
+ userlist = self.samdb.search(base=self.samdb.domain_dn(),
+ scope=ldb.SCOPE_SUBTREE,
+ expression=search_filter)
+ if userlist:
+ return userlist[0]
+ else:
+ return None
diff --git a/python/samba/tests/samba_tool/user_auth_policy.py b/python/samba/tests/samba_tool/user_auth_policy.py
new file mode 100644
index 0000000..c5bdd06
--- /dev/null
+++ b/python/samba/tests/samba_tool/user_auth_policy.py
@@ -0,0 +1,86 @@
+# Unix SMB/CIFS implementation.
+#
+# Tests for samba-tool user auth policy command
+#
+# Copyright (C) Catalyst.Net Ltd. 2023
+#
+# Written by Rob van der Linde <rob@catalyst.net.nz>
+#
+# 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.netcmd.domain.models import AuthenticationPolicy, User
+
+from .silo_base import SiloTest
+
+
+class AuthPolicyCmdTestCase(SiloTest):
+ def test_assign(self):
+ """Test assigning an authentication policy to a user."""
+ self.addCleanup(self.runcmd, "user", "auth", "policy", "remove", "alice")
+ result, out, err = self.runcmd("user", "auth", "policy", "assign",
+ "alice", "--policy", "User Policy")
+ self.assertIsNone(result, msg=err)
+
+ # Assigned policy should be 'Developers'
+ user = User.get(self.samdb, username="alice")
+ policy = AuthenticationPolicy.get(self.samdb, dn=user.assigned_policy)
+ self.assertEqual(policy.name, "User Policy")
+
+ def test_assign__invalid_policy(self):
+ """Test assigning a non-existing authentication policy to a user."""
+ result, out, err = self.runcmd("user", "auth", "policy", "assign",
+ "alice", "--policy", "doesNotExist")
+ self.assertEqual(result, -1)
+ self.assertIn("Authentication policy doesNotExist not found.", err)
+
+ def test_remove(self):
+ """Test removing the assigned authentication policy from a user."""
+ # First assign a policy, so we can test removing it.
+ self.runcmd("user", "auth", "policy", "assign", "bob", "--policy",
+ "User Policy")
+
+ # Assigned policy should be set
+ user = User.get(self.samdb, username="bob")
+ self.assertIsNotNone(user.assigned_policy)
+
+ # Now try removing it
+ result, out, err = self.runcmd("user", "auth", "policy", "remove",
+ "bob")
+ self.assertIsNone(result, msg=err)
+
+ # Assigned policy should be None
+ user = User.get(self.samdb, username="bob")
+ self.assertIsNone(user.assigned_policy)
+
+ def test_view(self):
+ """Test viewing the current assigned authentication policy on a user."""
+ # Assign a policy on one of the users.
+ self.addCleanup(self.runcmd, "user", "auth", "policy", "remove", "bob")
+ self.runcmd("user", "auth", "policy", "assign", "bob", "--policy",
+ "User Policy")
+
+ # Test user with a policy assigned.
+ result, out, err = self.runcmd("user", "auth", "policy", "view",
+ "bob")
+ self.assertIsNone(result, msg=err)
+ self.assertEqual(
+ out, "User bob assigned to authentication policy User Policy\n")
+
+ # Test user without a policy assigned.
+ result, out, err = self.runcmd("user", "auth", "policy", "view",
+ "joe")
+ self.assertIsNone(result, msg=err)
+ self.assertEqual(
+ out, "User joe has no assigned authentication policy.\n")
diff --git a/python/samba/tests/samba_tool/user_auth_silo.py b/python/samba/tests/samba_tool/user_auth_silo.py
new file mode 100644
index 0000000..19cce26
--- /dev/null
+++ b/python/samba/tests/samba_tool/user_auth_silo.py
@@ -0,0 +1,84 @@
+# Unix SMB/CIFS implementation.
+#
+# Tests for samba-tool user auth silo command
+#
+# Copyright (C) Catalyst.Net Ltd. 2023
+#
+# Written by Rob van der Linde <rob@catalyst.net.nz>
+#
+# 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.netcmd.domain.models import AuthenticationSilo, User
+
+from .silo_base import SiloTest
+
+
+class AuthPolicyCmdTestCase(SiloTest):
+ def test_assign(self):
+ """Test assigning an authentication silo to a user."""
+ self.addCleanup(self.runcmd, "user", "auth", "silo", "remove", "alice")
+ result, out, err = self.runcmd("user", "auth", "silo", "assign",
+ "alice", "--silo", "Developers")
+ self.assertIsNone(result, msg=err)
+
+ # Assigned silo should be 'Developers'
+ user = User.get(self.samdb, username="alice")
+ silo = AuthenticationSilo.get(self.samdb, dn=user.assigned_silo)
+ self.assertEqual(silo.name, "Developers")
+
+ def test_assign__invalid_silo(self):
+ """Test assigning a non-existing authentication silo to a user."""
+ result, out, err = self.runcmd("user", "auth", "silo", "assign",
+ "alice", "--silo", "doesNotExist")
+ self.assertEqual(result, -1)
+ self.assertIn("Authentication silo doesNotExist not found.", err)
+
+ def test_remove(self):
+ """Test removing the assigned authentication silo from a user."""
+ # First assign a silo, so we can test removing it.
+ self.runcmd("user", "auth", "silo", "assign", "bob", "--silo", "QA")
+
+ # Assigned silo should be set
+ user = User.get(self.samdb, username="bob")
+ self.assertIsNotNone(user.assigned_silo)
+
+ # Now try removing it
+ result, out, err = self.runcmd("user", "auth", "silo", "remove",
+ "bob")
+ self.assertIsNone(result, msg=err)
+
+ # Assigned silo should be None
+ user = User.get(self.samdb, username="bob")
+ self.assertIsNone(user.assigned_silo)
+
+ def test_view(self):
+ """Test viewing the current assigned authentication silo on a user."""
+ # Assign a silo on one of the users.
+ self.addCleanup(self.runcmd, "user", "auth", "silo", "remove", "bob")
+ self.runcmd("user", "auth", "silo", "assign", "bob", "--silo", "QA")
+
+ # Test user with a silo assigned.
+ result, out, err = self.runcmd("user", "auth", "silo", "view",
+ "bob")
+ self.assertIsNone(result, msg=err)
+ self.assertEqual(
+ out, "User bob assigned to authentication silo QA (revoked)\n")
+
+ # Test user without a silo assigned.
+ result, out, err = self.runcmd("user", "auth", "silo", "view",
+ "joe")
+ self.assertIsNone(result, msg=err)
+ self.assertEqual(
+ out, "User joe has no assigned authentication silo.\n")
diff --git a/python/samba/tests/samba_tool/user_check_password_script.py b/python/samba/tests/samba_tool/user_check_password_script.py
new file mode 100644
index 0000000..183b77b
--- /dev/null
+++ b/python/samba/tests/samba_tool/user_check_password_script.py
@@ -0,0 +1,106 @@
+# Unix SMB/CIFS implementation.
+# Copyright (C) Sean Dague <sdague@linux.vnet.ibm.com> 2011
+# Copyright (C) Andrew Bartlett <abartlet@samba.org> 2016
+#
+# 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 os
+from samba.tests.samba_tool.base import SambaToolCmdTest
+
+
+class UserCheckPwdTestCase(SambaToolCmdTest):
+ """Tests for samba-tool user subcommands"""
+ users = []
+ samdb = None
+
+ def setUp(self):
+ super().setUp()
+ self.samdb = self.getSamDB("-H", "ldap://%s" % os.environ["DC_SERVER"],
+ "-U%s%%%s" % (os.environ["DC_USERNAME"], os.environ["DC_PASSWORD"]))
+ self.old_min_pwd_age = self.samdb.get_minPwdAge()
+ self.samdb.set_minPwdAge("0")
+
+ def tearDown(self):
+ super().tearDown()
+ self.samdb.set_minPwdAge(self.old_min_pwd_age)
+
+ def _test_checkpassword(self, user, bad_password, good_password, desc):
+
+ (result, out, err) = self.runsubcmd("user", "add", user["name"], bad_password,
+ "-H", "ldap://%s" % os.environ["DC_SERVER"],
+ "-U%s%%%s" % (os.environ["DC_USERNAME"], os.environ["DC_PASSWORD"]))
+ self.assertCmdFail(result, "Should fail adding a user with %s password." % desc)
+
+ (result, out, err) = self.runsubcmd("user", "add", user["name"], good_password,
+ "-H", "ldap://%s" % os.environ["DC_SERVER"],
+ "-U%s%%%s" % (os.environ["DC_USERNAME"], os.environ["DC_PASSWORD"]))
+ self.assertCmdSuccess(result, out, err, "Should succeed adding a user with good password.")
+
+ # Set password
+ (result, out, err) = self.runsubcmd("user", "setpassword", user["name"],
+ "--newpassword=%s" % bad_password,
+ "-H", "ldap://%s" % os.environ["DC_SERVER"],
+ "-U%s%%%s" % (os.environ["DC_USERNAME"], os.environ["DC_PASSWORD"]))
+ self.assertCmdFail(result, "Should fail setting a user's password to a %s password." % desc)
+
+ (result, out, err) = self.runsubcmd("user", "setpassword", user["name"],
+ "--newpassword=%s" % good_password,
+ "-H", "ldap://%s" % os.environ["DC_SERVER"],
+ "-U%s%%%s" % (os.environ["DC_USERNAME"], os.environ["DC_PASSWORD"]))
+ self.assertCmdSuccess(result, out, err, "Should succeed setting a user's password to a good one.")
+
+ # Password=
+
+ (result, out, err) = self.runsubcmd("user", "password",
+ "--newpassword=%s" % bad_password,
+ "--ipaddress", os.environ["DC_SERVER_IP"],
+ "-U%s%%%s" % (user["name"], good_password))
+ self.assertCmdFail(result, "A user setting their own password to a %s password should fail." % desc)
+
+ (result, out, err) = self.runsubcmd("user", "password",
+ "--newpassword=%s" % good_password + 'XYZ',
+ "--ipaddress", os.environ["DC_SERVER_IP"],
+ "-U%s%%%s" % (user["name"], good_password))
+ self.assertCmdSuccess(result, out, err, "A user setting their own password to a good one should succeed.")
+
+ def test_checkpassword_unacceptable(self):
+ # Add
+ user = self._randomUser()
+ bad_password = os.environ["UNACCEPTABLE_PASSWORD"]
+ good_password = bad_password[:-1]
+ return self._test_checkpassword(user,
+ bad_password,
+ good_password,
+ "unacceptable")
+
+ def test_checkpassword_username(self):
+ # Add
+ user = self._randomUser()
+ bad_password = user["name"]
+ good_password = bad_password[:-1]
+ return self._test_checkpassword(user,
+ bad_password,
+ good_password,
+ "username")
+
+ def _randomUser(self, base=None):
+ """create a user with random attribute values, you can specify base attributes"""
+ if base is None:
+ base = {}
+ user = {
+ "name": self.randomName(),
+ }
+ user.update(base)
+ return user
diff --git a/python/samba/tests/samba_tool/user_edit.sh b/python/samba/tests/samba_tool/user_edit.sh
new file mode 100755
index 0000000..342899f
--- /dev/null
+++ b/python/samba/tests/samba_tool/user_edit.sh
@@ -0,0 +1,198 @@
+#!/bin/sh
+#
+# Test for 'samba-tool user edit'
+
+if [ $# -lt 3 ]; then
+ cat <<EOF
+Usage: user_edit.sh SERVER USERNAME PASSWORD
+EOF
+ exit 1
+fi
+
+SERVER="$1"
+USERNAME="$2"
+PASSWORD="$3"
+
+samba_ldbsearch=ldbsearch
+if test -x $BINDIR/ldbsearch; then
+ samba_ldbsearch=$BINDIR/ldbsearch
+fi
+
+STpath=$(pwd)
+. $STpath/testprogs/blackbox/subunit.sh
+
+display_name="Björn"
+display_name_b64="QmrDtnJu"
+display_name_new="Renamed Bjoern"
+# attribute value including control character
+# echo -e "test \a string" | base64
+display_name_con_b64="dGVzdCAHIHN0cmluZwo="
+
+tmpeditor=$(mktemp --suffix .sh -p ${SELFTEST_TMPDIR} samba-tool-editor-XXXXXXXX)
+chmod +x $tmpeditor
+
+TEST_USER="$(mktemp -u sambatoolXXXXXX)"
+
+create_test_user()
+{
+ $PYTHON ${STpath}/source4/scripting/bin/samba-tool \
+ user create ${TEST_USER} --random-password \
+ -H "ldap://$SERVER" "-U$USERNAME" "--password=$PASSWORD"
+}
+
+edit_user()
+{
+ # create editor.sh
+ cat >$tmpeditor <<-'EOF'
+#!/usr/bin/env bash
+user_ldif="$1"
+SED=$(which sed)
+$SED -i -e 's/userAccountControl: 512/userAccountControl: 514/' $user_ldif
+ EOF
+
+ $PYTHON ${STpath}/source4/scripting/bin/samba-tool \
+ user edit ${TEST_USER} --editor=$tmpeditor \
+ -H "ldap://$SERVER" "-U$USERNAME" "--password=$PASSWORD"
+}
+
+# Test edit user - add base64 attributes
+add_attribute_base64()
+{
+ # create editor.sh
+ cat >$tmpeditor <<EOF
+#!/usr/bin/env bash
+user_ldif="\$1"
+
+grep -v '^\$' \$user_ldif > \${user_ldif}.tmp
+echo "displayName:: $display_name_b64" >> \${user_ldif}.tmp
+
+mv \${user_ldif}.tmp \$user_ldif
+EOF
+
+ $PYTHON ${STpath}/source4/scripting/bin/samba-tool user edit \
+ ${TEST_USER} --editor=$tmpeditor \
+ -H "ldap://$SERVER" "-U$USERNAME" "--password=$PASSWORD"
+}
+
+get_attribute_base64()
+{
+ $samba_ldbsearch "(sAMAccountName=${TEST_USER})" displayName \
+ -H "ldap://$SERVER" "-U$USERNAME" "--password=$PASSWORD"
+}
+
+delete_attribute()
+{
+ # create editor.sh
+ cat >$tmpeditor <<EOF
+#!/usr/bin/env bash
+user_ldif="\$1"
+
+grep -v '^displayName' \$user_ldif >> \${user_ldif}.tmp
+mv \${user_ldif}.tmp \$user_ldif
+EOF
+ $PYTHON ${STpath}/source4/scripting/bin/samba-tool user edit \
+ ${TEST_USER} --editor=$tmpeditor \
+ -H "ldap://$SERVER" "-U$USERNAME" "--password=$PASSWORD"
+}
+
+# Test edit user - add base64 attribute value including control character
+add_attribute_base64_control()
+{
+ # create editor.sh
+ cat >$tmpeditor <<EOF
+#!/usr/bin/env bash
+user_ldif="\$1"
+
+grep -v '^\$' \$user_ldif > \${user_ldif}.tmp
+echo "displayName:: $display_name_con_b64" >> \${user_ldif}.tmp
+
+mv \${user_ldif}.tmp \$user_ldif
+EOF
+ $PYTHON ${STpath}/source4/scripting/bin/samba-tool user edit \
+ ${TEST_USER} --editor=$tmpeditor \
+ -H "ldap://$SERVER" "-U$USERNAME" "--password=$PASSWORD"
+}
+
+get_attribute_base64_control()
+{
+ $PYTHON ${STpath}/source4/scripting/bin/samba-tool user show \
+ ${TEST_USER} --attributes=displayName \
+ -H "ldap://$SERVER" "-U$USERNAME" "--password=$PASSWORD"
+}
+
+get_attribute_force_no_base64()
+{
+ # LDB_FLAG_FORCE_NO_BASE64_LDIF should be used here.
+ $PYTHON ${STpath}/source4/scripting/bin/samba-tool user show \
+ ${TEST_USER} --attributes=displayName \
+ -H "ldap://$SERVER" "-U$USERNAME" "--password=$PASSWORD"
+}
+
+# Test edit user - change base64 attribute value including control character
+change_attribute_base64_control()
+{
+ # create editor.sh
+ cat >$tmpeditor <<EOF
+#!/usr/bin/env bash
+user_ldif="\$1"
+
+sed -i -e 's/displayName:: $display_name_con_b64/displayName: $display_name/' \
+ \$user_ldif
+EOF
+ $PYTHON ${STpath}/source4/scripting/bin/samba-tool user edit \
+ ${TEST_USER} --editor=$tmpeditor \
+ -H "ldap://$SERVER" "-U$USERNAME" "--password=$PASSWORD"
+}
+
+# Test edit user - change attributes with LDB_FLAG_FORCE_NO_BASE64_LDIF
+change_attribute_force_no_base64()
+{
+ # create editor.sh
+ # Expects that the original attribute is available as clear text,
+ # because the LDB_FLAG_FORCE_NO_BASE64_LDIF should be used here.
+ cat >$tmpeditor <<EOF
+#!/usr/bin/env bash
+user_ldif="\$1"
+
+sed -i -e 's/displayName: $display_name/displayName: $display_name_new/' \
+ \$user_ldif
+EOF
+
+ $PYTHON ${STpath}/source4/scripting/bin/samba-tool user edit \
+ ${TEST_USER} --editor=$tmpeditor \
+ -H "ldap://$SERVER" "-U$USERNAME" "--password=$PASSWORD"
+}
+
+get_changed_attribute_force_no_base64()
+{
+ $PYTHON ${STpath}/source4/scripting/bin/samba-tool user show \
+ ${TEST_USER} --attributes=displayName \
+ -H "ldap://$SERVER" "-U$USERNAME" "--password=$PASSWORD"
+}
+
+delete_user()
+{
+ $PYTHON ${STpath}/source4/scripting/bin/samba-tool \
+ user delete ${TEST_USER} \
+ -H "ldap://$SERVER" "-U$USERNAME" "--password=$PASSWORD"
+}
+
+failed=0
+
+testit "create_test_user" create_test_user || failed=$(expr $failed + 1)
+testit "edit_user" edit_user || failed=$(expr $failed + 1)
+testit "add_attribute_base64" add_attribute_base64 || failed=$(expr $failed + 1)
+testit_grep "get_attribute_base64" "^displayName:: $display_name_b64" get_attribute_base64 || failed=$(expr $failed + 1)
+testit "delete_attribute" delete_attribute || failed=$(expr $failed + 1)
+testit "add_attribute_base64_control" add_attribute_base64_control || failed=$(expr $failed + 1)
+testit_grep "get_attribute_base64_control" "^displayName:: $display_name_con_b64" get_attribute_base64_control || failed=$(expr $failed + 1)
+testit "change_attribute_base64_control" change_attribute_base64_control || failed=$(expr $failed + 1)
+testit_grep "get_attribute_base64" "^displayName:: $display_name_b64" get_attribute_base64 || failed=$(expr $failed + 1)
+testit_grep "get_attribute_force_no_base64" "^displayName: $display_name" get_attribute_force_no_base64 || failed=$(expr $failed + 1)
+testit "change_attribute_force_no_base64" change_attribute_force_no_base64 || failed=$(expr $failed + 1)
+testit_grep "get_changed_attribute_force_no_base64" "^displayName: $display_name_new" get_changed_attribute_force_no_base64 || failed=$(expr $failed + 1)
+testit "delete_user" delete_user || failed=$(expr $failed + 1)
+
+rm -f $tmpeditor
+
+exit $failed
diff --git a/python/samba/tests/samba_tool/user_get_kerberos_ticket.py b/python/samba/tests/samba_tool/user_get_kerberos_ticket.py
new file mode 100644
index 0000000..4ac502e
--- /dev/null
+++ b/python/samba/tests/samba_tool/user_get_kerberos_ticket.py
@@ -0,0 +1,195 @@
+# Unix SMB/CIFS implementation.
+#
+# Blackbox tests for getting Kerberos tickets from Group Managed Service Account and other (local) passwords
+#
+# Copyright (C) Catalyst.Net Ltd. 2023
+#
+# Written by Rob van der Linde <rob@catalyst.net.nz>
+#
+# Copyright Andrew Bartlett <abartlet@samba.org> 2023
+#
+# 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 sys
+import os
+
+sys.path.insert(0, "bin/python")
+os.environ["PYTHONUNBUFFERED"] = "1"
+
+from ldb import SCOPE_BASE
+from samba import credentials
+from samba.credentials import Credentials, MUST_USE_KERBEROS
+from samba.dcerpc import security, samr
+from samba.dsdb import UF_WORKSTATION_TRUST_ACCOUNT, UF_NORMAL_ACCOUNT
+from samba.netcmd.domain.models import User
+from samba.ndr import ndr_pack, ndr_unpack
+from samba.tests import connect_samdb, connect_samdb_env, delete_force
+
+from samba.tests import BlackboxTestCase, BlackboxProcessError
+
+
+# If not specified, this is None, meaning local sam.ldb
+PW_READ_URL = os.environ.get("PW_READ_URL")
+
+# We still need to connect to a remote server to check we got the ticket
+SERVER = os.environ.get("SERVER")
+
+PW_CHECK_URL = f"ldap://{SERVER}"
+
+# For authentication to PW_READ_URL if required
+SERVER_USERNAME = os.environ["USERNAME"]
+SERVER_PASSWORD = os.environ["PASSWORD"]
+
+CREDS = f"-U{SERVER_USERNAME}%{SERVER_PASSWORD}"
+
+
+class GetKerberosTiketTest(BlackboxTestCase):
+ """Blackbox tests for GMSA getpassword and connecting as that user."""
+
+ @classmethod
+ def setUpClass(cls):
+ cls.lp = cls.get_loadparm()
+ cls.env_creds = cls.get_env_credentials(lp=cls.lp,
+ env_username="USERNAME",
+ env_password="PASSWORD",
+ env_domain="DOMAIN",
+ env_realm="REALM")
+ if PW_READ_URL is None:
+ url = cls.lp.private_path("sam.ldb")
+ else:
+ url = PW_CHECK_URL
+ cls.samdb = connect_samdb(url, lp=cls.lp, credentials=cls.env_creds)
+ super().setUpClass()
+
+ @classmethod
+ def setUpTestData(cls):
+ cls.gmsa_username = "GMSA_K5Test_User$"
+ cls.username = "get-kerberos-ticket-test"
+ cls.user_base_dn = f"CN=Users,{cls.samdb.domain_dn()}"
+ cls.user_dn = f"CN={cls.username},{cls.user_base_dn}"
+ cls.gmsa_base_dn = f"CN=Managed Service Accounts,{cls.samdb.domain_dn()}"
+ cls.gmsa_user_dn = f"CN={cls.gmsa_username},{cls.gmsa_base_dn}"
+
+ msg = cls.samdb.search(base="", scope=SCOPE_BASE, attrs=["tokenGroups"])[0]
+ connecting_user_sid = str(ndr_unpack(security.dom_sid, msg["tokenGroups"][0]))
+
+ domain_sid = security.dom_sid(cls.samdb.get_domain_sid())
+ allow_sddl = f"O:SYD:(A;;RP;;;{connecting_user_sid})"
+ allow_sd = ndr_pack(security.descriptor.from_sddl(allow_sddl, domain_sid))
+
+ details = {
+ "dn": str(cls.gmsa_user_dn),
+ "objectClass": "msDS-GroupManagedServiceAccount",
+ "msDS-ManagedPasswordInterval": "1",
+ "msDS-GroupMSAMembership": allow_sd,
+ "sAMAccountName": cls.gmsa_username,
+ "userAccountControl": str(UF_WORKSTATION_TRUST_ACCOUNT),
+ }
+
+ cls.samdb.add(details)
+ cls.addClassCleanup(delete_force, cls.samdb, cls.gmsa_user_dn)
+
+ user_password = "P@ssw0rd"
+ utf16pw = ('"' + user_password + '"').encode('utf-16-le')
+ user_details = {
+ "dn": str(cls.user_dn),
+ "objectClass": "user",
+ "sAMAccountName": cls.username,
+ "userAccountControl": str(UF_NORMAL_ACCOUNT),
+ "unicodePwd": utf16pw
+ }
+
+ cls.samdb.add(user_details)
+ cls.addClassCleanup(delete_force, cls.samdb, cls.user_dn)
+
+ cls.gmsa_user = User.get(cls.samdb, username=cls.gmsa_username)
+ cls.user = User.get(cls.samdb, username=cls.username)
+
+ def get_ticket(self, username, options=None):
+ if options is None:
+ options = ""
+ ccache_path = f"{self.tempdir}/ccache"
+ ccache_location = f"FILE:{ccache_path}"
+ cmd = f"user get-kerberos-ticket --output-krb5-ccache={ccache_location} {username} {options}"
+
+ try:
+ self.check_output(cmd)
+ except BlackboxProcessError as e:
+ self.fail(e)
+ self.addCleanup(os.unlink, ccache_path)
+ return ccache_location
+
+ def test_gmsa_ticket(self):
+ # Get a ticket with the tool
+ output_ccache = self.get_ticket(self.gmsa_username)
+ creds = self.insta_creds(template=self.env_creds)
+ creds.set_kerberos_state(MUST_USE_KERBEROS)
+ creds.set_named_ccache(self.lp, output_ccache)
+ db = connect_samdb(PW_CHECK_URL, credentials=creds, lp=self.lp)
+ msg = db.search(base="", scope=SCOPE_BASE, attrs=["tokenGroups"])[0]
+ connecting_user_sid = str(ndr_unpack(security.dom_sid, msg["tokenGroups"][0]))
+
+ self.assertEqual(self.gmsa_user.object_sid, connecting_user_sid)
+
+ def test_user_ticket(self):
+ output_ccache = self.get_ticket(self.username)
+ # Get a ticket with the tool
+ creds = self.insta_creds(template=self.env_creds)
+ creds.set_kerberos_state(MUST_USE_KERBEROS)
+
+ # Currently this is based on reading the unicodePwd, but this should be expanded
+ creds.set_named_ccache(output_ccache, credentials.SPECIFIED, self.lp)
+
+ db = connect_samdb(PW_CHECK_URL, credentials=creds, lp=self.lp)
+
+ msg = db.search(base="", scope=SCOPE_BASE, attrs=["tokenGroups"])[0]
+ connecting_user_sid = str(ndr_unpack(security.dom_sid, msg["tokenGroups"][0]))
+
+ self.assertEqual(self.user.object_sid, connecting_user_sid)
+
+ def test_user_ticket_gpg(self):
+ output_ccache = self.get_ticket(self.username, "--decrypt-samba-gpg")
+ # Get a ticket with the tool
+ creds = self.insta_creds(template=self.env_creds)
+ creds.set_kerberos_state(MUST_USE_KERBEROS)
+ creds.set_named_ccache(output_ccache, credentials.SPECIFIED, self.lp)
+ db = connect_samdb(PW_CHECK_URL, credentials=creds, lp=self.lp)
+
+ msg = db.search(base="", scope=SCOPE_BASE, attrs=["tokenGroups"])[0]
+ connecting_user_sid = str(ndr_unpack(security.dom_sid, msg["tokenGroups"][0]))
+
+ self.assertEqual(self.user.object_sid, connecting_user_sid)
+
+ @classmethod
+ def _make_cmdline(cls, line):
+ """Override to pass line as samba-tool subcommand instead.
+
+ Automatically fills in HOST and CREDS as well.
+ """
+ if isinstance(line, list):
+ cmd = ["samba-tool"] + line
+ if PW_READ_URL is not None:
+ cmd += ["-H", PW_READ_URL, CREDS]
+ else:
+ cmd = f"samba-tool {line}"
+ if PW_READ_URL is not None:
+ cmd += "-H {PW_READ_URL} {CREDS}"
+
+ return super()._make_cmdline(cmd)
+
+
+if __name__ == "__main__":
+ import unittest
+ unittest.main()
diff --git a/python/samba/tests/samba_tool/user_getpassword_gmsa.py b/python/samba/tests/samba_tool/user_getpassword_gmsa.py
new file mode 100644
index 0000000..9844456
--- /dev/null
+++ b/python/samba/tests/samba_tool/user_getpassword_gmsa.py
@@ -0,0 +1,171 @@
+# Unix SMB/CIFS implementation.
+#
+# Blackbox tests for reading Group Managed Service Account passwords
+#
+# Copyright (C) Catalyst.Net Ltd. 2023
+#
+# Written by Rob van der Linde <rob@catalyst.net.nz>
+#
+# 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 sys
+import os
+
+sys.path.insert(0, "bin/python")
+os.environ["PYTHONUNBUFFERED"] = "1"
+
+from ldb import SCOPE_BASE
+
+from samba.credentials import Credentials, MUST_USE_KERBEROS
+from samba.dcerpc import security, samr
+from samba.dsdb import UF_WORKSTATION_TRUST_ACCOUNT
+from samba.netcmd.domain.models import User
+from samba.ndr import ndr_pack, ndr_unpack
+from samba.tests import connect_samdb, connect_samdb_env, delete_force
+
+from samba.tests import BlackboxTestCase
+
+DC_SERVER = os.environ["SERVER"]
+SERVER = os.environ["SERVER"]
+SERVER_USERNAME = os.environ["USERNAME"]
+SERVER_PASSWORD = os.environ["PASSWORD"]
+
+HOST = f"ldap://{SERVER}"
+CREDS = f"-U{SERVER_USERNAME}%{SERVER_PASSWORD}"
+
+
+class GMSAPasswordTest(BlackboxTestCase):
+ """Blackbox tests for GMSA getpassword and connecting as that user."""
+
+ @classmethod
+ def setUpClass(cls):
+ cls.lp = cls.get_loadparm()
+ cls.env_creds = cls.get_env_credentials(lp=cls.lp,
+ env_username="USERNAME",
+ env_password="PASSWORD",
+ env_domain="DOMAIN",
+ env_realm="REALM")
+ cls.samdb = connect_samdb(HOST, lp=cls.lp, credentials=cls.env_creds)
+ super().setUpClass()
+
+ @classmethod
+ def setUpTestData(cls):
+ cls.username = "GMSA_Test_User$"
+ cls.base_dn = f"CN=Managed Service Accounts,{cls.samdb.domain_dn()}"
+ cls.user_dn = f"CN={cls.username},{cls.base_dn}"
+
+ msg = cls.samdb.search(base="", scope=SCOPE_BASE, attrs=["tokenGroups"])[0]
+ connecting_user_sid = str(ndr_unpack(security.dom_sid, msg["tokenGroups"][0]))
+
+ domain_sid = security.dom_sid(cls.samdb.get_domain_sid())
+ allow_sddl = f"O:SYD:(A;;RP;;;{connecting_user_sid})"
+ allow_sd = ndr_pack(security.descriptor.from_sddl(allow_sddl, domain_sid))
+
+ details = {
+ "dn": str(cls.user_dn),
+ "objectClass": "msDS-GroupManagedServiceAccount",
+ "msDS-ManagedPasswordInterval": "1",
+ "msDS-GroupMSAMembership": allow_sd,
+ "sAMAccountName": cls.username,
+ "userAccountControl": str(UF_WORKSTATION_TRUST_ACCOUNT),
+ }
+
+ cls.samdb.add(details)
+ cls.addClassCleanup(delete_force, cls.samdb, cls.user_dn)
+
+ cls.user = User.get(cls.samdb, username=cls.username)
+
+ def getpassword(self, attrs):
+ cmd = f"user getpassword --attributes={attrs} {self.username}"
+
+ ldif = self.check_output(cmd).decode()
+ res = self.samdb.parse_ldif(ldif)
+ _, user_message = next(res)
+
+ # check each attr is returned
+ for attr in attrs.split(","):
+ self.assertIn(attr, user_message)
+
+ return user_message
+
+ def test_getpassword(self):
+ self.getpassword("virtualClearTextUTF16,unicodePwd")
+ self.getpassword("virtualClearTextUTF16")
+ self.getpassword("unicodePwd")
+
+ def test_utf16_password(self):
+ user_msg = self.getpassword("virtualClearTextUTF16")
+ password = user_msg["virtualClearTextUTF16"][0]
+
+ creds = self.insta_creds(template=self.env_creds)
+ creds.set_username(self.username)
+ creds.set_utf16_password(password)
+ db = connect_samdb(HOST, credentials=creds, lp=self.lp)
+
+ msg = db.search(base="", scope=SCOPE_BASE, attrs=["tokenGroups"])[0]
+ connecting_user_sid = str(ndr_unpack(security.dom_sid, msg["tokenGroups"][0]))
+
+ self.assertEqual(self.user.object_sid, connecting_user_sid)
+
+ def test_utf8_password(self):
+ user_msg = self.getpassword("virtualClearTextUTF8")
+ password = str(user_msg["virtualClearTextUTF8"][0])
+
+ creds = self.insta_creds(template=self.env_creds)
+ # Because the password has been converted to utf-8 via UTF16_MUNGED
+ # the nthash is no longer valid. We need to use AES kerberos ciphers
+ # for this to work.
+ creds.set_kerberos_state(MUST_USE_KERBEROS)
+ creds.set_username(self.username)
+ creds.set_password(password)
+ db = connect_samdb(HOST, credentials=creds, lp=self.lp)
+
+ msg = db.search(base="", scope=SCOPE_BASE, attrs=["tokenGroups"])[0]
+ connecting_user_sid = str(ndr_unpack(security.dom_sid, msg["tokenGroups"][0]))
+
+ self.assertEqual(self.user.object_sid, connecting_user_sid)
+
+ def test_unicode_pwd(self):
+ user_msg = self.getpassword("unicodePwd")
+
+ creds = self.insta_creds(template=self.env_creds)
+ creds.set_username(self.username)
+ nt_pass = samr.Password()
+ nt_pass.hash = list(user_msg["unicodePwd"][0])
+ creds.set_nt_hash(nt_pass)
+ db = connect_samdb(HOST, credentials=creds, lp=self.lp)
+
+ msg = db.search(base="", scope=SCOPE_BASE, attrs=["tokenGroups"])[0]
+ connecting_user_sid = str(ndr_unpack(security.dom_sid, msg["tokenGroups"][0]))
+
+ self.assertEqual(self.user.object_sid, connecting_user_sid)
+
+ @classmethod
+ def _make_cmdline(cls, line):
+ """Override to pass line as samba-tool subcommand instead.
+
+ Automatically fills in HOST and CREDS as well.
+ """
+ if isinstance(line, list):
+ cmd = ["samba-tool"] + line + ["-H", SERVER, CREDS]
+ else:
+ cmd = f"samba-tool {line} -H {HOST} {CREDS}"
+
+ return super()._make_cmdline(cmd)
+
+
+if __name__ == "__main__":
+ import unittest
+ unittest.main()
diff --git a/python/samba/tests/samba_tool/user_virtualCryptSHA.py b/python/samba/tests/samba_tool/user_virtualCryptSHA.py
new file mode 100644
index 0000000..e95a4be
--- /dev/null
+++ b/python/samba/tests/samba_tool/user_virtualCryptSHA.py
@@ -0,0 +1,516 @@
+# Tests for the samba-tool user sub command reading Primary:userPassword
+#
+# Copyright (C) Andrew Bartlett <abartlet@samba.org> 2017
+#
+# 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 ldb
+import samba
+from samba.tests.samba_tool.base import SambaToolCmdTest
+from samba.credentials import Credentials
+from samba.samdb import SamDB
+from samba.auth import system_session
+from samba import dsdb
+
+USER_NAME = "CryptSHATestUser"
+HASH_OPTION = "password hash userPassword schemes"
+
+
+class UserCmdCryptShaTestCase(SambaToolCmdTest):
+ """
+ Tests for samba-tool user subcommands generation of the virtualCryptSHA256
+ and virtualCryptSHA512 attributes
+ """
+ users = []
+ samdb = None
+
+ def add_user(self, hashes=""):
+ self.lp = samba.tests.env_loadparm()
+
+ # set the extra hashes to be calculated
+ self.lp.set(HASH_OPTION, hashes)
+
+ self.creds = Credentials()
+ self.session = system_session()
+ self.ldb = SamDB(
+ session_info=self.session,
+ credentials=self.creds,
+ lp=self.lp)
+
+ password = self.random_password()
+ self.runsubcmd("user",
+ "create",
+ USER_NAME,
+ password)
+
+ def tearDown(self):
+ super().tearDown()
+ self.runsubcmd("user", "delete", USER_NAME)
+
+ def _get_password(self, attributes, decrypt=False):
+ command = ["user",
+ "getpassword",
+ USER_NAME,
+ "--attributes",
+ attributes]
+ if decrypt:
+ command.append("--decrypt-samba-gpg")
+
+ (result, out, err) = self.runsubcmd(*command)
+ self.assertCmdSuccess(result,
+ out,
+ err,
+ "Ensure getpassword runs")
+ self.assertEqual(err, "Got password OK\n", "getpassword")
+ return out
+
+ # Change the just the NT password hash, as would happen if the password
+ # was updated by Windows, the userPassword values are now obsolete.
+ #
+ def _change_nt_hash(self):
+ res = self.ldb.search(expression = "cn=%s" % USER_NAME,
+ scope = ldb.SCOPE_SUBTREE)
+ msg = ldb.Message()
+ msg.dn = res[0].dn
+ msg["unicodePwd"] = ldb.MessageElement(b"ABCDEF1234567890",
+ ldb.FLAG_MOD_REPLACE,
+ "unicodePwd")
+ self.ldb.modify(
+ msg,
+ controls=["local_oid:%s:0" %
+ dsdb.DSDB_CONTROL_BYPASS_PASSWORD_HASH_OID])
+
+ # gpg decryption not enabled.
+ # both virtual attributes specified, no rounds option
+ # no hashes stored in supplementalCredentials
+ # Should not get values
+ def test_no_gpg_both_hashes_no_rounds(self):
+ self.add_user()
+ out = self._get_password("virtualCryptSHA256,virtualCryptSHA512")
+
+ self.assertTrue("virtualCryptSHA256:" not in out)
+ self.assertTrue("virtualCryptSHA512:" not in out)
+ self.assertTrue("rounds=" not in out)
+
+ # gpg decryption not enabled.
+ # SHA256 specified
+ # no hashes stored in supplementalCredentials
+ # No rounds
+ #
+ # Should not get values
+ def test_no_gpg_sha256_no_rounds(self):
+ self.add_user()
+ out = self._get_password("virtualCryptSHA256")
+
+ self.assertTrue("virtualCryptSHA256:" not in out)
+ self.assertTrue("virtualCryptSHA512:" not in out)
+ self.assertTrue("rounds=" not in out)
+
+ # gpg decryption not enabled.
+ # SHA512 specified
+ # no hashes stored in supplementalCredentials
+ # No rounds
+ #
+ # Should not get values
+ def test_no_gpg_sha512_no_rounds(self):
+ self.add_user()
+ out = self._get_password("virtualCryptSHA512")
+
+ self.assertTrue("virtualCryptSHA256:" not in out)
+ self.assertTrue("virtualCryptSHA512:" not in out)
+ self.assertTrue("rounds=" not in out)
+
+ # gpg decryption not enabled.
+ # SHA128 specified, i.e. invalid/unknown algorithm
+ # no hashes stored in supplementalCredentials
+ # No rounds
+ #
+ # Should not get values
+ def test_no_gpg_invalid_alg_no_rounds(self):
+ self.add_user()
+ out = self._get_password("virtualCryptSHA128")
+
+ self.assertTrue("virtualCryptSHA256:" not in out)
+ self.assertTrue("virtualCryptSHA512:" not in out)
+ self.assertTrue("rounds=" not in out)
+
+ # gpg decryption enabled.
+ # both virtual attributes specified, no rounds option
+ # no hashes stored in supplementalCredentials
+ # Should get values
+ def test_gpg_both_hashes_no_rounds(self):
+ self.add_user()
+ out = self._get_password("virtualCryptSHA256,virtualCryptSHA512", True)
+
+ self.assertTrue("virtualCryptSHA256:" in out)
+ self.assertTrue("virtualCryptSHA512:" in out)
+ self.assertTrue("rounds=" not in out)
+
+ # gpg decryption enabled.
+ # SHA256 specified
+ # no hashes stored in supplementalCredentials
+ # No rounds
+ #
+ # Should get values
+ def test_gpg_sha256_no_rounds(self):
+ self.add_user()
+ out = self._get_password("virtualCryptSHA256", True)
+
+ self.assertTrue("virtualCryptSHA256:" in out)
+ self.assertTrue("virtualCryptSHA512:" not in out)
+ self.assertTrue("rounds=" not in out)
+
+ # gpg decryption enabled.
+ # SHA512 specified
+ # no hashes stored in supplementalCredentials
+ # No rounds
+ #
+ # Should get values
+ def test_gpg_sha512_no_rounds(self):
+ self.add_user()
+ out = self._get_password("virtualCryptSHA512", True)
+
+ self.assertTrue("virtualCryptSHA256:" not in out)
+ self.assertTrue("virtualCryptSHA512:" in out)
+ self.assertTrue("rounds=" not in out)
+
+ # gpg decryption enabled.
+ # SHA128 specified, i.e. invalid/unknown algorithm
+ # no hashes stored in supplementalCredentials
+ # No rounds
+ #
+ # Should not get values
+ def test_gpg_invalid_alg_no_rounds(self):
+ self.add_user()
+ out = self._get_password("virtualCryptSHA128", True)
+
+ self.assertTrue("virtualCryptSHA256:" not in out)
+ self.assertTrue("virtualCryptSHA512:" not in out)
+ self.assertTrue("rounds=" not in out)
+
+ # gpg decryption enabled.
+ # both virtual attributes specified, no rounds option
+ # no hashes stored in supplementalCredentials
+ # underlying windows password changed, so plain text password is
+ # invalid.
+ # Should not get values
+ def test_gpg_both_hashes_no_rounds_pwd_changed(self):
+ self.add_user()
+ self._change_nt_hash()
+ out = self._get_password("virtualCryptSHA256,virtualCryptSHA512", True)
+
+ self.assertTrue("virtualCryptSHA256:" not in out)
+ self.assertTrue("virtualCryptSHA512:" not in out)
+ self.assertTrue("rounds=" not in out)
+
+ # gpg decryption enabled.
+ # SHA256 specified, no rounds option
+ # no hashes stored in supplementalCredentials
+ # underlying windows password changed, so plain text password is
+ # invalid.
+ # Should not get values
+ def test_gpg_sha256_no_rounds_pwd_changed(self):
+ self.add_user()
+ self._change_nt_hash()
+ out = self._get_password("virtualCryptSHA256", True)
+
+ self.assertTrue("virtualCryptSHA256:" not in out)
+ self.assertTrue("virtualCryptSHA512:" not in out)
+ self.assertTrue("rounds=" not in out)
+
+ # gpg decryption enabled.
+ # SHA512 specified, no rounds option
+ # no hashes stored in supplementalCredentials
+ # underlying windows password changed, so plain text password is
+ # invalid.
+ # Should not get values
+ def test_gpg_sha512_no_rounds_pwd_changed(self):
+ self.add_user()
+ self._change_nt_hash()
+ out = self._get_password("virtualCryptSHA256", True)
+
+ self.assertTrue("virtualCryptSHA256:" not in out)
+ self.assertTrue("virtualCryptSHA512:" not in out)
+ self.assertTrue("rounds=" not in out)
+
+ # gpg decryption enabled.
+ # both virtual attributes specified, rounds specified
+ # no hashes stored in supplementalCredentials
+ # Should get values reflecting the requested rounds
+ def test_gpg_both_hashes_both_rounds(self):
+ self.add_user()
+ out = self._get_password(
+ "virtualCryptSHA256;rounds=10123,virtualCryptSHA512;rounds=10456",
+ True)
+
+ self.assertTrue("virtualCryptSHA256:" in out)
+ self.assertTrue("virtualCryptSHA512:" in out)
+
+ sha256 = self._get_attribute(out, "virtualCryptSHA256")
+ self.assertTrue(sha256.startswith("{CRYPT}$5$rounds=10123$"))
+
+ sha512 = self._get_attribute(out, "virtualCryptSHA512")
+ self.assertTrue(sha512.startswith("{CRYPT}$6$rounds=10456$"))
+
+ # gpg decryption enabled.
+ # both virtual attributes specified, rounds specified
+ # invalid rounds for sha256
+ # no hashes stored in supplementalCredentials
+ # Should get values, no rounds for sha256, rounds for sha 512
+ def test_gpg_both_hashes_sha256_rounds_invalid(self):
+ self.add_user()
+ out = self._get_password(
+ "virtualCryptSHA256;rounds=invalid,virtualCryptSHA512;rounds=3125",
+ True)
+
+ self.assertTrue("virtualCryptSHA256:" in out)
+ self.assertTrue("virtualCryptSHA512:" in out)
+
+ sha256 = self._get_attribute(out, "virtualCryptSHA256")
+ self.assertTrue(sha256.startswith("{CRYPT}$5$"))
+ self.assertTrue("rounds" not in sha256)
+
+ sha512 = self._get_attribute(out, "virtualCryptSHA512")
+ self.assertTrue(sha512.startswith("{CRYPT}$6$rounds=3125$"))
+
+ # gpg decryption not enabled.
+ # both virtual attributes specified, no rounds option
+ # both hashes stored in supplementalCredentials
+ # Should get values
+ def test_no_gpg_both_hashes_no_rounds_stored_hashes(self):
+ self.add_user("CryptSHA512 CryptSHA256")
+
+ out = self._get_password("virtualCryptSHA256,virtualCryptSHA512")
+
+ self.assertTrue("virtualCryptSHA256:" in out)
+ self.assertTrue("virtualCryptSHA512:" in out)
+ self.assertTrue("rounds=" not in out)
+
+ # Should be using the pre computed hash in supplementalCredentials
+ # so it should not change between calls.
+ sha256 = self._get_attribute(out, "virtualCryptSHA256")
+ sha512 = self._get_attribute(out, "virtualCryptSHA512")
+
+ out = self._get_password("virtualCryptSHA256,virtualCryptSHA512")
+ self.assertEqual(sha256, self._get_attribute(out, "virtualCryptSHA256"))
+ self.assertEqual(sha512, self._get_attribute(out, "virtualCryptSHA512"))
+
+ # gpg decryption not enabled.
+ # both virtual attributes specified, rounds specified
+ # both hashes stored in supplementalCredentials, with not rounds
+ # Should get hashes for the first matching scheme entry
+ def test_no_gpg_both_hashes_rounds_stored_hashes(self):
+ self.add_user("CryptSHA512 CryptSHA256")
+
+ out = self._get_password("virtualCryptSHA256;rounds=2561," +
+ "virtualCryptSHA512;rounds=5129")
+
+ self.assertTrue("virtualCryptSHA256:" in out)
+ self.assertTrue("virtualCryptSHA512:" in out)
+ self.assertTrue("rounds=" not in out)
+
+ # Should be using the pre computed hash in supplementalCredentials
+ # so it should not change between calls.
+ sha256 = self._get_attribute(out, "virtualCryptSHA256")
+ sha512 = self._get_attribute(out, "virtualCryptSHA512")
+
+ out = self._get_password("virtualCryptSHA256,virtualCryptSHA512")
+ self.assertEqual(sha256, self._get_attribute(out, "virtualCryptSHA256"))
+ self.assertEqual(sha512, self._get_attribute(out, "virtualCryptSHA512"))
+
+ # gpg decryption not enabled.
+ # both virtual attributes specified, rounds specified
+ # both hashes stored in supplementalCredentials, with rounds
+ # Should get values
+ def test_no_gpg_both_hashes_rounds_stored_hashes_with_rounds(self):
+ self.add_user("CryptSHA512 " +
+ "CryptSHA256 " +
+ "CryptSHA512:rounds=5129 " +
+ "CryptSHA256:rounds=2561")
+
+ out = self._get_password("virtualCryptSHA256;rounds=2561," +
+ "virtualCryptSHA512;rounds=5129")
+
+ self.assertTrue("virtualCryptSHA256:" in out)
+ self.assertTrue("virtualCryptSHA512:" in out)
+ self.assertTrue("rounds=" in out)
+
+ # Should be using the pre computed hash in supplementalCredentials
+ # so it should not change between calls.
+ sha256 = self._get_attribute(out, "virtualCryptSHA256")
+ sha512 = self._get_attribute(out, "virtualCryptSHA512")
+
+ out = self._get_password("virtualCryptSHA256;rounds=2561," +
+ "virtualCryptSHA512;rounds=5129")
+ self.assertEqual(sha256, self._get_attribute(out, "virtualCryptSHA256"))
+ self.assertEqual(sha512, self._get_attribute(out, "virtualCryptSHA512"))
+
+ # Number of rounds should match that specified
+ self.assertTrue(sha256.startswith("{CRYPT}$5$rounds=2561"))
+ self.assertTrue(sha512.startswith("{CRYPT}$6$rounds=5129"))
+
+ # gpg decryption not enabled.
+ # both virtual attributes specified, rounds specified
+ # both hashes stored in supplementalCredentials, with rounds
+ # number of rounds stored/requested do not match
+ # Should get the precomputed hashes for CryptSHA512 and CryptSHA256
+ def test_no_gpg_both_hashes_rounds_stored_hashes_with_rounds_no_match(self):
+ self.add_user("CryptSHA512 " +
+ "CryptSHA256 " +
+ "CryptSHA512:rounds=5129 " +
+ "CryptSHA256:rounds=2561")
+
+ out = self._get_password("virtualCryptSHA256;rounds=4000," +
+ "virtualCryptSHA512;rounds=5000")
+
+ self.assertTrue("virtualCryptSHA256:" in out)
+ self.assertTrue("virtualCryptSHA512:" in out)
+ self.assertTrue("rounds=" not in out)
+
+ # Should be using the pre computed hash in supplementalCredentials
+ # so it should not change between calls.
+ sha256 = self._get_attribute(out, "virtualCryptSHA256")
+ sha512 = self._get_attribute(out, "virtualCryptSHA512")
+
+ out = self._get_password("virtualCryptSHA256;rounds=4000," +
+ "virtualCryptSHA512;rounds=5000")
+ self.assertEqual(sha256, self._get_attribute(out, "virtualCryptSHA256"))
+ self.assertEqual(sha512, self._get_attribute(out, "virtualCryptSHA512"))
+
+ # As the number of rounds did not match, should have returned the
+ # first hash of the corresponding scheme
+ out = self._get_password("virtualCryptSHA256," +
+ "virtualCryptSHA512")
+ self.assertEqual(sha256, self._get_attribute(out, "virtualCryptSHA256"))
+ self.assertEqual(sha512, self._get_attribute(out, "virtualCryptSHA512"))
+
+ # gpg decryption enabled.
+ # both virtual attributes specified, no rounds option
+ # both hashes stored in supplementalCredentials
+ # Should get values
+ def test_gpg_both_hashes_no_rounds_stored_hashes(self):
+ self.add_user("CryptSHA512 CryptSHA256")
+
+ out = self._get_password("virtualCryptSHA256,virtualCryptSHA512", True)
+
+ self.assertTrue("virtualCryptSHA256:" in out)
+ self.assertTrue("virtualCryptSHA512:" in out)
+ self.assertTrue("rounds=" not in out)
+
+ # Should be using the pre computed hash in supplementalCredentials
+ # so it should not change between calls.
+ sha256 = self._get_attribute(out, "virtualCryptSHA256")
+ sha512 = self._get_attribute(out, "virtualCryptSHA512")
+
+ out = self._get_password("virtualCryptSHA256,virtualCryptSHA512", True)
+ self.assertEqual(sha256, self._get_attribute(out, "virtualCryptSHA256"))
+ self.assertEqual(sha512, self._get_attribute(out, "virtualCryptSHA512"))
+
+ # gpg decryption enabled.
+ # both virtual attributes specified, rounds specified
+ # both hashes stored in supplementalCredentials, with no rounds
+ # Should get calculated hashed with the correct number of rounds
+ def test_gpg_both_hashes_rounds_stored_hashes(self):
+ self.add_user("CryptSHA512 CryptSHA256")
+
+ out = self._get_password("virtualCryptSHA256;rounds=2561," +
+ "virtualCryptSHA512;rounds=5129",
+ True)
+
+ self.assertTrue("virtualCryptSHA256:" in out)
+ self.assertTrue("virtualCryptSHA512:" in out)
+ self.assertTrue("rounds=" in out)
+
+ # Should be calculating the hashes
+ # so they should change between calls.
+ sha256 = self._get_attribute(out, "virtualCryptSHA256")
+ sha512 = self._get_attribute(out, "virtualCryptSHA512")
+
+ out = self._get_password("virtualCryptSHA256;rounds=2561," +
+ "virtualCryptSHA512;rounds=5129",
+ True)
+ self.assertFalse(sha256 == self._get_attribute(out, "virtualCryptSHA256"))
+ self.assertFalse(sha512 == self._get_attribute(out, "virtualCryptSHA512"))
+
+ # The returned hashes should specify the correct number of rounds
+ self.assertTrue(sha256.startswith("{CRYPT}$5$rounds=2561"))
+ self.assertTrue(sha512.startswith("{CRYPT}$6$rounds=5129"))
+
+ # gpg decryption enabled.
+ # both virtual attributes specified, rounds specified
+ # both hashes stored in supplementalCredentials, with rounds
+ # Should get values
+ def test_gpg_both_hashes_rounds_stored_hashes_with_rounds(self):
+ self.add_user("CryptSHA512 " +
+ "CryptSHA256 " +
+ "CryptSHA512:rounds=5129 " +
+ "CryptSHA256:rounds=2561")
+
+ out = self._get_password("virtualCryptSHA256;rounds=2561," +
+ "virtualCryptSHA512;rounds=5129",
+ True)
+
+ self.assertTrue("virtualCryptSHA256:" in out)
+ self.assertTrue("virtualCryptSHA512:" in out)
+ self.assertTrue("rounds=" in out)
+
+ # Should be using the pre computed hash in supplementalCredentials
+ # so it should not change between calls.
+ sha256 = self._get_attribute(out, "virtualCryptSHA256")
+ sha512 = self._get_attribute(out, "virtualCryptSHA512")
+
+ out = self._get_password("virtualCryptSHA256;rounds=2561," +
+ "virtualCryptSHA512;rounds=5129",
+ True)
+ self.assertEqual(sha256, self._get_attribute(out, "virtualCryptSHA256"))
+ self.assertEqual(sha512, self._get_attribute(out, "virtualCryptSHA512"))
+
+ # The returned hashes should specify the correct number of rounds
+ self.assertTrue(sha256.startswith("{CRYPT}$5$rounds=2561"))
+ self.assertTrue(sha512.startswith("{CRYPT}$6$rounds=5129"))
+
+ # gpg decryption enabled.
+ # both virtual attributes specified, rounds specified
+ # both hashes stored in supplementalCredentials, with rounds
+ # number of rounds stored/requested do not match
+ # Should get calculated hashes with the correct number of rounds
+ def test_gpg_both_hashes_rounds_stored_hashes_with_rounds_no_match(self):
+ self.add_user("CryptSHA512 " +
+ "CryptSHA256 " +
+ "CryptSHA512:rounds=5129 " +
+ "CryptSHA256:rounds=2561")
+
+ out = self._get_password("virtualCryptSHA256;rounds=4000," +
+ "virtualCryptSHA512;rounds=5000",
+ True)
+
+ self.assertTrue("virtualCryptSHA256:" in out)
+ self.assertTrue("virtualCryptSHA512:" in out)
+ self.assertTrue("rounds=" in out)
+
+ # Should be calculating the hashes
+ # so they should change between calls.
+ sha256 = self._get_attribute(out, "virtualCryptSHA256")
+ sha512 = self._get_attribute(out, "virtualCryptSHA512")
+
+ out = self._get_password("virtualCryptSHA256;rounds=4000," +
+ "virtualCryptSHA512;rounds=5000",
+ True)
+ self.assertFalse(sha256 == self._get_attribute(out, "virtualCryptSHA256"))
+ self.assertFalse(sha512 == self._get_attribute(out, "virtualCryptSHA512"))
+
+ # The calculated hashes should specify the correct number of rounds
+ self.assertTrue(sha256.startswith("{CRYPT}$5$rounds=4000"))
+ self.assertTrue(sha512.startswith("{CRYPT}$6$rounds=5000"))
diff --git a/python/samba/tests/samba_tool/user_virtualCryptSHA_base.py b/python/samba/tests/samba_tool/user_virtualCryptSHA_base.py
new file mode 100644
index 0000000..14e3de9
--- /dev/null
+++ b/python/samba/tests/samba_tool/user_virtualCryptSHA_base.py
@@ -0,0 +1,99 @@
+# Tests for the samba-tool user sub command reading Primary:userPassword
+#
+# Copyright (C) Andrew Bartlett <abartlet@samba.org> 2017
+#
+# 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 ldb
+import samba
+from samba.tests.samba_tool.base import SambaToolCmdTest
+from samba.credentials import Credentials
+from samba.samdb import SamDB
+from samba.auth import system_session
+from samba import dsdb
+
+USER_NAME = "CryptSHATestUser"
+HASH_OPTION = "password hash userPassword schemes"
+
+
+class UserCmdCryptShaTestCase(SambaToolCmdTest):
+ """
+ Tests for samba-tool user subcommands generation of the virtualCryptSHA256
+ and virtualCryptSHA512 attributes
+ """
+ users = []
+ samdb = None
+
+ def _get_attribute(self, out, name):
+ parsed = list(self.ldb.parse_ldif(out))
+ self.assertEqual(len(parsed), 1)
+ changetype, msg = parsed[0]
+ return str(msg.get(name, ""))
+
+ def add_user(self, hashes=""):
+ self.lp = samba.tests.env_loadparm()
+
+ # set the extra hashes to be calculated
+ self.lp.set(HASH_OPTION, hashes)
+
+ self.creds = Credentials()
+ self.session = system_session()
+ self.ldb = SamDB(
+ session_info=self.session,
+ credentials=self.creds,
+ lp=self.lp)
+
+ password = self.random_password()
+ self.runsubcmd("user",
+ "create",
+ USER_NAME,
+ password)
+
+ def tearDown(self):
+ super().tearDown()
+ self.runsubcmd("user", "delete", USER_NAME)
+
+ def _get_password(self, attributes, decrypt=False):
+ command = ["user",
+ "getpassword",
+ USER_NAME,
+ "--attributes",
+ attributes]
+ if decrypt:
+ command.append("--decrypt-samba-gpg")
+
+ (result, out, err) = self.runsubcmd(*command)
+ self.assertCmdSuccess(result,
+ out,
+ err,
+ "Ensure getpassword runs")
+ self.assertEqual(err, "Got password OK\n", "getpassword")
+ return out
+
+ # Change the just the NT password hash, as would happen if the password
+ # was updated by Windows, the userPassword values are now obsolete.
+ #
+ def _change_nt_hash(self):
+ res = self.ldb.search(expression = "cn=%s" % USER_NAME,
+ scope = ldb.SCOPE_SUBTREE)
+ msg = ldb.Message()
+ msg.dn = res[0].dn
+ msg["unicodePwd"] = ldb.MessageElement(b"ABCDEF1234567890",
+ ldb.FLAG_MOD_REPLACE,
+ "unicodePwd")
+ self.ldb.modify(
+ msg,
+ controls=["local_oid:%s:0" %
+ dsdb.DSDB_CONTROL_BYPASS_PASSWORD_HASH_OID])
diff --git a/python/samba/tests/samba_tool/user_virtualCryptSHA_gpg.py b/python/samba/tests/samba_tool/user_virtualCryptSHA_gpg.py
new file mode 100644
index 0000000..6517eee
--- /dev/null
+++ b/python/samba/tests/samba_tool/user_virtualCryptSHA_gpg.py
@@ -0,0 +1,262 @@
+# Tests for the samba-tool user sub command reading Primary:userPassword
+#
+# Copyright (C) Andrew Bartlett <abartlet@samba.org> 2017
+#
+# 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.tests.samba_tool.user_virtualCryptSHA_base import UserCmdCryptShaTestCase
+
+
+class UserCmdCryptShaTestCaseGPG(UserCmdCryptShaTestCase):
+ """
+ Tests for samba-tool user subcommands generation of the virtualCryptSHA256
+ and virtualCryptSHA512 attributes
+ """
+
+ # gpg decryption enabled.
+ # both virtual attributes specified, no rounds option
+ # no hashes stored in supplementalCredentials
+ # Should get values
+ def test_gpg_both_hashes_no_rounds(self):
+ self.add_user()
+ out = self._get_password("virtualCryptSHA256,virtualCryptSHA512", True)
+
+ self.assertIn("virtualCryptSHA256:", out)
+ self.assertIn("virtualCryptSHA512:", out)
+ self.assertNotIn("rounds=", out)
+
+ # gpg decryption enabled.
+ # SHA256 specified
+ # no hashes stored in supplementalCredentials
+ # No rounds
+ #
+ # Should get values
+ def test_gpg_sha256_no_rounds(self):
+ self.add_user()
+ out = self._get_password("virtualCryptSHA256", True)
+
+ self.assertIn("virtualCryptSHA256:", out)
+ self.assertNotIn("virtualCryptSHA512:", out)
+ self.assertNotIn("rounds=", out)
+
+ # gpg decryption enabled.
+ # SHA512 specified
+ # no hashes stored in supplementalCredentials
+ # No rounds
+ #
+ # Should get values
+ def test_gpg_sha512_no_rounds(self):
+ self.add_user()
+ out = self._get_password("virtualCryptSHA512", True)
+
+ self.assertNotIn("virtualCryptSHA256:", out)
+ self.assertIn("virtualCryptSHA512:",out)
+ self.assertNotIn("rounds=", out)
+
+ # gpg decryption enabled.
+ # SHA128 specified, i.e. invalid/unknown algorithm
+ # no hashes stored in supplementalCredentials
+ # No rounds
+ #
+ # Should not get values
+ def test_gpg_invalid_alg_no_rounds(self):
+ self.add_user()
+ out = self._get_password("virtualCryptSHA128", True)
+
+ self.assertNotIn("virtualCryptSHA256:", out)
+ self.assertNotIn("virtualCryptSHA512:", out)
+ self.assertNotIn("rounds=", out)
+
+ # gpg decryption enabled.
+ # both virtual attributes specified, no rounds option
+ # no hashes stored in supplementalCredentials
+ # underlying windows password changed, so plain text password is
+ # invalid.
+ # Should not get values
+ def test_gpg_both_hashes_no_rounds_pwd_changed(self):
+ self.add_user()
+ self._change_nt_hash()
+ out = self._get_password("virtualCryptSHA256,virtualCryptSHA512", True)
+
+ self.assertNotIn("virtualCryptSHA256:", out)
+ self.assertNotIn("virtualCryptSHA512:", out)
+ self.assertNotIn("rounds=", out)
+
+ # gpg decryption enabled.
+ # SHA256 specified, no rounds option
+ # no hashes stored in supplementalCredentials
+ # underlying windows password changed, so plain text password is
+ # invalid.
+ # Should not get values
+ def test_gpg_sha256_no_rounds_pwd_changed(self):
+ self.add_user()
+ self._change_nt_hash()
+ out = self._get_password("virtualCryptSHA256", True)
+
+ self.assertNotIn("virtualCryptSHA256:", out)
+ self.assertNotIn("virtualCryptSHA512:", out)
+ self.assertNotIn("rounds=", out)
+
+ # gpg decryption enabled.
+ # SHA512 specified, no rounds option
+ # no hashes stored in supplementalCredentials
+ # underlying windows password changed, so plain text password is
+ # invalid.
+ # Should not get values
+ def test_gpg_sha512_no_rounds_pwd_changed(self):
+ self.add_user()
+ self._change_nt_hash()
+ out = self._get_password("virtualCryptSHA256", True)
+
+ self.assertNotIn("virtualCryptSHA256:", out)
+ self.assertNotIn("virtualCryptSHA512:", out)
+ self.assertNotIn("rounds=", out)
+
+ # gpg decryption enabled.
+ # both virtual attributes specified, rounds specified
+ # no hashes stored in supplementalCredentials
+ # Should get values reflecting the requested rounds
+ def test_gpg_both_hashes_both_rounds(self):
+ self.add_user()
+ out = self._get_password(
+ "virtualCryptSHA256;rounds=10123,virtualCryptSHA512;rounds=10456",
+ True)
+
+ self.assertIn("virtualCryptSHA256;rounds=10123:", out)
+ self.assertIn("virtualCryptSHA512;rounds=10456:", out)
+
+ sha256 = self._get_attribute(out, "virtualCryptSHA256;rounds=10123")
+ self.assertTrue(sha256.startswith("{CRYPT}$5$rounds=10123$"))
+
+ sha512 = self._get_attribute(out, "virtualCryptSHA512;rounds=10456")
+ self.assertTrue(sha512.startswith("{CRYPT}$6$rounds=10456$"))
+
+ # gpg decryption enabled.
+ # both virtual attributes specified, rounds specified
+ # invalid rounds for sha256
+ # no hashes stored in supplementalCredentials
+ # Should get values, no rounds for sha256, rounds for sha 512
+ def test_gpg_both_hashes_sha256_rounds_invalid(self):
+ self.add_user()
+ out = self._get_password(
+ "virtualCryptSHA256;rounds=invalid,virtualCryptSHA512;rounds=3125",
+ True)
+
+ self.assertIn("virtualCryptSHA256;rounds=invalid:", out)
+ self.assertIn("virtualCryptSHA512;rounds=3125:", out)
+
+ sha256 = self._get_attribute(out, "virtualCryptSHA256;rounds=invalid")
+ self.assertTrue(sha256.startswith("{CRYPT}$5$"))
+ self.assertNotIn("rounds", sha256)
+
+ sha512 = self._get_attribute(out, "virtualCryptSHA512;rounds=3125")
+ self.assertTrue(sha512.startswith("{CRYPT}$6$rounds=3125$"))
+
+ # gpg decryption enabled.
+ # both virtual attributes specified, rounds specified
+ # both hashes stored in supplementalCredentials, with no rounds
+ # Should get calculated hashed with the correct number of rounds
+ def test_gpg_both_hashes_rounds_stored_hashes(self):
+ self.add_user("CryptSHA512 CryptSHA256")
+
+ out = self._get_password("virtualCryptSHA256;rounds=2561," +
+ "virtualCryptSHA512;rounds=5129",
+ True)
+
+ self.assertIn("virtualCryptSHA256;rounds=2561:", out)
+ self.assertIn("virtualCryptSHA512;rounds=5129:", out)
+ self.assertIn("$rounds=", out)
+
+ # Should be calculating the hashes
+ # so they should change between calls.
+ sha256 = self._get_attribute(out, "virtualCryptSHA256;rounds=2561")
+ sha512 = self._get_attribute(out, "virtualCryptSHA512;rounds=5129")
+
+ out = self._get_password("virtualCryptSHA256;rounds=2561," +
+ "virtualCryptSHA512;rounds=5129",
+ True)
+ self.assertNotEqual(sha256, self._get_attribute(out, "virtualCryptSHA256"))
+ self.assertNotEqual(sha512, self._get_attribute(out, "virtualCryptSHA512"))
+
+ # The returned hashes should specify the correct number of rounds
+ self.assertTrue(sha256.startswith("{CRYPT}$5$rounds=2561"))
+ self.assertTrue(sha512.startswith("{CRYPT}$6$rounds=5129"))
+
+ # gpg decryption enabled.
+ # both virtual attributes specified, rounds specified
+ # both hashes stored in supplementalCredentials, with rounds
+ # Should get values
+ def test_gpg_both_hashes_rounds_stored_hashes_with_rounds(self):
+ self.add_user("CryptSHA512 " +
+ "CryptSHA256 " +
+ "CryptSHA512:rounds=5129 " +
+ "CryptSHA256:rounds=2561")
+
+ out = self._get_password("virtualCryptSHA256;rounds=2561," +
+ "virtualCryptSHA512;rounds=5129",
+ True)
+
+ self.assertIn("virtualCryptSHA256;rounds=2561:", out)
+ self.assertIn("virtualCryptSHA512;rounds=5129:", out)
+ self.assertIn("$rounds=", out)
+
+ # Should be using the pre computed hash in supplementalCredentials
+ # so it should not change between calls.
+ sha256 = self._get_attribute(out, "virtualCryptSHA256;rounds=2561")
+ sha512 = self._get_attribute(out, "virtualCryptSHA512;rounds=5129")
+
+ out = self._get_password("virtualCryptSHA256;rounds=2561," +
+ "virtualCryptSHA512;rounds=5129",
+ True)
+ self.assertEqual(sha256, self._get_attribute(out, "virtualCryptSHA256;rounds=2561"))
+ self.assertEqual(sha512, self._get_attribute(out, "virtualCryptSHA512;rounds=5129"))
+
+ # The returned hashes should specify the correct number of rounds
+ self.assertTrue(sha256.startswith("{CRYPT}$5$rounds=2561"))
+ self.assertTrue(sha512.startswith("{CRYPT}$6$rounds=5129"))
+
+ # gpg decryption enabled.
+ # both virtual attributes specified, rounds specified
+ # both hashes stored in supplementalCredentials, with rounds
+ # number of rounds stored/requested do not match
+ # Should get calculated hashes with the correct number of rounds
+ def test_gpg_both_hashes_rounds_stored_hashes_with_rounds_no_match(self):
+ self.add_user("CryptSHA512 " +
+ "CryptSHA256 " +
+ "CryptSHA512:rounds=5129 " +
+ "CryptSHA256:rounds=2561")
+
+ out = self._get_password("virtualCryptSHA256;rounds=4000," +
+ "virtualCryptSHA512;rounds=5000",
+ True)
+
+ self.assertIn("virtualCryptSHA256;rounds=4000:", out)
+ self.assertIn("virtualCryptSHA512;rounds=5000:", out)
+ self.assertIn("$rounds=", out)
+
+ # Should be calculating the hashes
+ # so they should change between calls.
+ sha256 = self._get_attribute(out, "virtualCryptSHA256;rounds=4000")
+ sha512 = self._get_attribute(out, "virtualCryptSHA512;rounds=5000")
+
+ out = self._get_password("virtualCryptSHA256;rounds=4000," +
+ "virtualCryptSHA512;rounds=5000",
+ True)
+ self.assertNotEqual(sha256, self._get_attribute(out, "virtualCryptSHA256;rounds=4000"))
+ self.assertNotEqual(sha512, self._get_attribute(out, "virtualCryptSHA512;rounds=5000"))
+
+ # The calculated hashes should specify the correct number of rounds
+ self.assertTrue(sha256.startswith("{CRYPT}$5$rounds=4000"))
+ self.assertTrue(sha512.startswith("{CRYPT}$6$rounds=5000"))
diff --git a/python/samba/tests/samba_tool/user_virtualCryptSHA_userPassword.py b/python/samba/tests/samba_tool/user_virtualCryptSHA_userPassword.py
new file mode 100644
index 0000000..1f84af0
--- /dev/null
+++ b/python/samba/tests/samba_tool/user_virtualCryptSHA_userPassword.py
@@ -0,0 +1,188 @@
+# Tests for the samba-tool user sub command reading Primary:userPassword
+#
+# Copyright (C) Andrew Bartlett <abartlet@samba.org> 2017
+#
+# 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.tests.samba_tool.user_virtualCryptSHA_base import UserCmdCryptShaTestCase
+
+
+class UserCmdCryptShaTestCaseUserPassword(UserCmdCryptShaTestCase):
+ # gpg decryption not enabled.
+ # both virtual attributes specified, no rounds option
+ # no hashes stored in supplementalCredentials
+ # Should not get values
+ def test_no_gpg_both_hashes_no_rounds(self):
+ self.add_user()
+ out = self._get_password("virtualCryptSHA256,virtualCryptSHA512")
+
+ self.assertTrue("virtualCryptSHA256:" not in out)
+ self.assertTrue("virtualCryptSHA512:" not in out)
+ self.assertTrue("rounds=" not in out)
+
+ # gpg decryption not enabled.
+ # SHA256 specified
+ # no hashes stored in supplementalCredentials
+ # No rounds
+ #
+ # Should not get values
+ def test_no_gpg_sha256_no_rounds(self):
+ self.add_user()
+ out = self._get_password("virtualCryptSHA256")
+
+ self.assertTrue("virtualCryptSHA256:" not in out)
+ self.assertTrue("virtualCryptSHA512:" not in out)
+ self.assertTrue("rounds=" not in out)
+
+ # gpg decryption not enabled.
+ # SHA512 specified
+ # no hashes stored in supplementalCredentials
+ # No rounds
+ #
+ # Should not get values
+ def test_no_gpg_sha512_no_rounds(self):
+ self.add_user()
+ out = self._get_password("virtualCryptSHA512")
+
+ self.assertTrue("virtualCryptSHA256:" not in out)
+ self.assertTrue("virtualCryptSHA512:" not in out)
+ self.assertTrue("rounds=" not in out)
+
+ # gpg decryption not enabled.
+ # SHA128 specified, i.e. invalid/unknown algorithm
+ # no hashes stored in supplementalCredentials
+ # No rounds
+ #
+ # Should not get values
+ def test_no_gpg_invalid_alg_no_rounds(self):
+ self.add_user()
+ out = self._get_password("virtualCryptSHA128")
+
+ self.assertTrue("virtualCryptSHA256:" not in out)
+ self.assertTrue("virtualCryptSHA512:" not in out)
+ self.assertTrue("rounds=" not in out)
+
+ # gpg decryption not enabled.
+ # both virtual attributes specified, no rounds option
+ # both hashes stored in supplementalCredentials
+ # Should get values
+ def test_no_gpg_both_hashes_no_rounds_stored_hashes(self):
+ self.add_user("CryptSHA512 CryptSHA256")
+
+ out = self._get_password("virtualCryptSHA256,virtualCryptSHA512")
+
+ self.assertTrue("virtualCryptSHA256:" in out)
+ self.assertTrue("virtualCryptSHA512:" in out)
+ self.assertTrue("rounds=" not in out)
+
+ # Should be using the pre computed hash in supplementalCredentials
+ # so it should not change between calls.
+ sha256 = self._get_attribute(out, "virtualCryptSHA256")
+ sha512 = self._get_attribute(out, "virtualCryptSHA512")
+
+ out = self._get_password("virtualCryptSHA256,virtualCryptSHA512")
+ self.assertEqual(sha256, self._get_attribute(out, "virtualCryptSHA256"))
+ self.assertEqual(sha512, self._get_attribute(out, "virtualCryptSHA512"))
+
+ # gpg decryption not enabled.
+ # both virtual attributes specified, rounds specified
+ # both hashes stored in supplementalCredentials, with not rounds
+ # Should get hashes for the first matching scheme entry
+ def test_no_gpg_both_hashes_rounds_stored_hashes(self):
+ self.add_user("CryptSHA512 CryptSHA256")
+
+ out = self._get_password("virtualCryptSHA256;rounds=2561," +
+ "virtualCryptSHA512;rounds=5129")
+
+ self.assertTrue("virtualCryptSHA256;rounds=2561:" in out)
+ self.assertTrue("virtualCryptSHA512;rounds=5129:" in out)
+ self.assertTrue("$rounds=" not in out)
+
+ # Should be using the pre computed hash in supplementalCredentials
+ # so it should not change between calls.
+ sha256 = self._get_attribute(out, "virtualCryptSHA256;rounds=2561")
+ sha512 = self._get_attribute(out, "virtualCryptSHA512;rounds=5129")
+
+ out = self._get_password("virtualCryptSHA256,virtualCryptSHA512")
+ self.assertEqual(sha256, self._get_attribute(out,
+ "virtualCryptSHA256"))
+ self.assertEqual(sha512, self._get_attribute(out,
+ "virtualCryptSHA512"))
+
+ # gpg decryption not enabled.
+ # both virtual attributes specified, rounds specified
+ # both hashes stored in supplementalCredentials, with rounds
+ # Should get values
+ def test_no_gpg_both_hashes_rounds_stored_hashes_with_rounds(self):
+ self.add_user("CryptSHA512 " +
+ "CryptSHA256 " +
+ "CryptSHA512:rounds=5129 " +
+ "CryptSHA256:rounds=2561")
+
+ out = self._get_password("virtualCryptSHA256;rounds=2561," +
+ "virtualCryptSHA512;rounds=5129")
+
+ self.assertTrue("virtualCryptSHA256;rounds=2561:" in out)
+ self.assertTrue("virtualCryptSHA512;rounds=5129:" in out)
+ self.assertTrue("$rounds=" in out)
+
+ # Should be using the pre computed hash in supplementalCredentials
+ # so it should not change between calls.
+ sha256 = self._get_attribute(out, "virtualCryptSHA256;rounds=2561")
+ sha512 = self._get_attribute(out, "virtualCryptSHA512;rounds=5129")
+
+ out = self._get_password("virtualCryptSHA256;rounds=2561," +
+ "virtualCryptSHA512;rounds=5129")
+ self.assertEqual(sha256, self._get_attribute(out, "virtualCryptSHA256;rounds=2561"))
+ self.assertEqual(sha512, self._get_attribute(out, "virtualCryptSHA512;rounds=5129"))
+
+ # Number of rounds should match that specified
+ self.assertTrue(sha256.startswith("{CRYPT}$5$rounds=2561"))
+ self.assertTrue(sha512.startswith("{CRYPT}$6$rounds=5129"))
+
+ # gpg decryption not enabled.
+ # both virtual attributes specified, rounds specified
+ # both hashes stored in supplementalCredentials, with rounds
+ # number of rounds stored/requested do not match
+ # Should get the precomputed hashes for CryptSHA512 and CryptSHA256
+ def test_no_gpg_both_hashes_rounds_stored_hashes_with_rounds_no_match(self):
+ self.add_user("CryptSHA512 " +
+ "CryptSHA256 " +
+ "CryptSHA512:rounds=5129 " +
+ "CryptSHA256:rounds=2561")
+
+ out = self._get_password("virtualCryptSHA256;rounds=4000," +
+ "virtualCryptSHA512;rounds=5000")
+
+ self.assertTrue("virtualCryptSHA256;rounds=4000:" in out)
+ self.assertTrue("virtualCryptSHA512;rounds=5000:" in out)
+ self.assertTrue("$rounds=" not in out)
+
+ # Should be using the pre computed hash in supplementalCredentials
+ # so it should not change between calls.
+ sha256 = self._get_attribute(out, "virtualCryptSHA256;rounds=4000")
+ sha512 = self._get_attribute(out, "virtualCryptSHA512;rounds=5000")
+
+ out = self._get_password("virtualCryptSHA256;rounds=4000," +
+ "virtualCryptSHA512;rounds=5000")
+ self.assertEqual(sha256, self._get_attribute(out, "virtualCryptSHA256;rounds=4000"))
+ self.assertEqual(sha512, self._get_attribute(out, "virtualCryptSHA512;rounds=5000"))
+
+ # As the number of rounds did not match, should have returned the
+ # first hash of the corresponding scheme
+ out = self._get_password("virtualCryptSHA256," +
+ "virtualCryptSHA512")
+ self.assertEqual(sha256, self._get_attribute(out, "virtualCryptSHA256"))
+ self.assertEqual(sha512, self._get_attribute(out, "virtualCryptSHA512"))
diff --git a/python/samba/tests/samba_tool/user_wdigest.py b/python/samba/tests/samba_tool/user_wdigest.py
new file mode 100644
index 0000000..0d87762
--- /dev/null
+++ b/python/samba/tests/samba_tool/user_wdigest.py
@@ -0,0 +1,450 @@
+# Tests for the samba-tool user sub command reading Primary:WDigest
+#
+# Copyright (C) Andrew Bartlett <abartlet@samba.org> 2017
+#
+#
+# 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 os
+import samba
+from samba.tests.samba_tool.base import SambaToolCmdTest
+from hashlib import md5
+
+
+USER_NAME = "WdigestTestUser"
+
+# Calculate the MD5 password digest from the supplied user, realm and password
+#
+
+
+def calc_digest(user, realm, password):
+ data = "%s:%s:%s" % (user, realm, password)
+ if isinstance(data, str):
+ data = data.encode('utf8')
+
+ return "%s:%s:%s" % (user, realm, md5(data).hexdigest())
+
+
+class UserCmdWdigestTestCase(SambaToolCmdTest):
+ """Tests for samba-tool user subcommands extraction of the wdigest values
+ Test results validated against Windows Server 2012 R2.
+ NOTE: That as at 22-05-2017 the values Documented at
+ 3.1.1.8.11.3.1 WDIGEST_CREDENTIALS Construction
+ are incorrect.
+ """
+ users = []
+ samdb = None
+
+ def setUp(self):
+ super().setUp()
+ self.lp = samba.tests.env_loadparm()
+ self.samdb = self.getSamDB(
+ "-H", "ldap://%s" % os.environ["DC_SERVER"],
+ "-U%s%%%s" % (os.environ["DC_USERNAME"],
+ os.environ["DC_PASSWORD"]))
+ self.dns_domain = self.samdb.domain_dns_name()
+ res = self.samdb.search(
+ base=self.samdb.get_config_basedn(),
+ expression="ncName=%s" % self.samdb.get_default_basedn(),
+ attrs=["nETBIOSName"])
+ self.netbios_domain = str(res[0]["nETBIOSName"][0])
+ self.password = self.random_password()
+ result, out, err = self.runsubcmd("user",
+ "create",
+ USER_NAME,
+ self.password)
+ self.assertCmdSuccess(result,
+ out,
+ err,
+ "Ensure user is created")
+
+ def tearDown(self):
+ super().tearDown()
+ result, out, err = self.runsubcmd("user", "delete", USER_NAME)
+ self.assertCmdSuccess(result,
+ out,
+ err,
+ "Ensure user is deleted")
+
+ def _testWDigest(self, attribute, expected, missing=False):
+
+ (result, out, err) = self.runsubcmd("user",
+ "getpassword",
+ USER_NAME,
+ "--attributes",
+ attribute)
+ self.assertCmdSuccess(result,
+ out,
+ err,
+ "Ensure getpassword runs")
+ self.assertEqual(err, "Got password OK\n", "getpassword")
+
+ if missing:
+ self.assertTrue(attribute not in out)
+ else:
+ self.assertMatch(out.replace('\n ', ''),
+ "%s: %s" % (attribute, expected))
+
+ def test_Wdigest_no_suffix(self):
+ attribute = "virtualWDigest"
+ self._testWDigest(attribute, None, True)
+
+ def test_Wdigest_non_numeric_suffix(self):
+ attribute = "virtualWDigestss"
+ self._testWDigest(attribute, None, True)
+
+ def test_Wdigest00(self):
+ attribute = "virtualWDigest00"
+ self._testWDigest(attribute, None, True)
+
+ # Hash01 MD5(sAMAccountName,
+ # NETBIOSDomainName,
+ # password)
+ #
+ def test_Wdigest01(self):
+ attribute = "virtualWDigest01"
+ expected = calc_digest(USER_NAME,
+ self.netbios_domain,
+ self.password)
+ self._testWDigest(attribute, expected)
+
+ # Hash02 MD5(LOWER(sAMAccountName),
+ # LOWER(NETBIOSDomainName),
+ # password)
+ #
+ def test_Wdigest02(self):
+ attribute = "virtualWDigest02"
+ expected = calc_digest(USER_NAME.lower(),
+ self.netbios_domain.lower(),
+ self.password)
+ self._testWDigest(attribute, expected)
+
+ # Hash03 MD5(UPPER(sAMAccountName),
+ # UPPER(NETBIOSDomainName),
+ # password)
+ #
+ def test_Wdigest03(self):
+ attribute = "virtualWDigest03"
+ expected = calc_digest(USER_NAME.upper(),
+ self.netbios_domain.upper(),
+ self.password)
+ self._testWDigest(attribute, expected)
+
+ # Hash04 MD5(sAMAccountName,
+ # UPPER(NETBIOSDomainName),
+ # password)
+ #
+ def test_Wdigest04(self):
+ attribute = "virtualWDigest04"
+ expected = calc_digest(USER_NAME,
+ self.netbios_domain.upper(),
+ self.password)
+ self._testWDigest(attribute, expected)
+
+ # Hash05 MD5(sAMAccountName,
+ # LOWER(NETBIOSDomainName),
+ # password)
+ #
+ def test_Wdigest05(self):
+ attribute = "virtualWDigest05"
+ expected = calc_digest(USER_NAME,
+ self.netbios_domain.lower(),
+ self.password)
+ self._testWDigest(attribute, expected)
+
+ # Hash06 MD5(UPPER(sAMAccountName),
+ # LOWER(NETBIOSDomainName),
+ # password)
+ #
+ def test_Wdigest06(self):
+ attribute = "virtualWDigest06"
+ expected = calc_digest(USER_NAME.upper(),
+ self.netbios_domain.lower(),
+ self.password)
+ self._testWDigest(attribute, expected)
+
+ # Hash07 MD5(LOWER(sAMAccountName),
+ # UPPER(NETBIOSDomainName),
+ # password)
+ #
+ def test_Wdigest07(self):
+ attribute = "virtualWDigest07"
+ expected = calc_digest(USER_NAME.lower(),
+ self.netbios_domain.upper(),
+ self.password)
+ self._testWDigest(attribute, expected)
+
+ # Hash08 MD5(sAMAccountName,
+ # DNSDomainName,
+ # password)
+ #
+ # Note: Samba lowercases the DNSDomainName at provision time,
+ # Windows preserves the case. This means that the WDigest08 values
+ # calculated byt Samba and Windows differ.
+ #
+ def test_Wdigest08(self):
+ attribute = "virtualWDigest08"
+ expected = calc_digest(USER_NAME,
+ self.dns_domain,
+ self.password)
+ self._testWDigest(attribute, expected)
+
+ # Hash09 MD5(LOWER(sAMAccountName),
+ # LOWER(DNSDomainName),
+ # password)
+ #
+ def test_Wdigest09(self):
+ attribute = "virtualWDigest09"
+ expected = calc_digest(USER_NAME.lower(),
+ self.dns_domain.lower(),
+ self.password)
+ self._testWDigest(attribute, expected)
+
+ # Hash10 MD5(UPPER(sAMAccountName),
+ # UPPER(DNSDomainName),
+ # password)
+ #
+ def test_Wdigest10(self):
+ attribute = "virtualWDigest10"
+ expected = calc_digest(USER_NAME.upper(),
+ self.dns_domain.upper(),
+ self.password)
+ self._testWDigest(attribute, expected)
+
+ # Hash11 MD5(sAMAccountName,
+ # UPPER(DNSDomainName),
+ # password)
+ #
+ def test_Wdigest11(self):
+ attribute = "virtualWDigest11"
+ expected = calc_digest(USER_NAME,
+ self.dns_domain.upper(),
+ self.password)
+ self._testWDigest(attribute, expected)
+
+ # Hash12 MD5(sAMAccountName,
+ # LOWER(DNSDomainName),
+ # password)
+ #
+ def test_Wdigest12(self):
+ attribute = "virtualWDigest12"
+ expected = calc_digest(USER_NAME,
+ self.dns_domain.lower(),
+ self.password)
+ self._testWDigest(attribute, expected)
+
+ # Hash13 MD5(UPPER(sAMAccountName),
+ # LOWER(DNSDomainName),
+ # password)
+ #
+ def test_Wdigest13(self):
+ attribute = "virtualWDigest13"
+ expected = calc_digest(USER_NAME.upper(),
+ self.dns_domain.lower(),
+ self.password)
+ self._testWDigest(attribute, expected)
+
+ # Hash14 MD5(LOWER(sAMAccountName),
+ # UPPER(DNSDomainName),
+ # password)
+ #
+
+ def test_Wdigest14(self):
+ attribute = "virtualWDigest14"
+ expected = calc_digest(USER_NAME.lower(),
+ self.dns_domain.upper(),
+ self.password)
+ self._testWDigest(attribute, expected)
+
+ # Hash15 MD5(userPrincipalName,
+ # password)
+ #
+ def test_Wdigest15(self):
+ attribute = "virtualWDigest15"
+ name = "%s@%s" % (USER_NAME, self.dns_domain)
+ expected = calc_digest(name,
+ "",
+ self.password)
+ self._testWDigest(attribute, expected)
+
+ # Hash16 MD5(LOWER(userPrincipalName),
+ # password)
+ #
+ def test_Wdigest16(self):
+ attribute = "virtualWDigest16"
+ name = "%s@%s" % (USER_NAME.lower(), self.dns_domain.lower())
+ expected = calc_digest(name,
+ "",
+ self.password)
+ self._testWDigest(attribute, expected)
+
+ # Hash17 MD5(UPPER(userPrincipalName),
+ # password)
+ #
+ def test_Wdigest17(self):
+ attribute = "virtualWDigest17"
+ name = "%s@%s" % (USER_NAME.upper(), self.dns_domain.upper())
+ expected = calc_digest(name,
+ "",
+ self.password)
+ self._testWDigest(attribute, expected)
+
+ # Hash18 MD5(NETBIOSDomainName\sAMAccountName,
+ # password)
+ #
+ def test_Wdigest18(self):
+ attribute = "virtualWDigest18"
+ name = "%s\\%s" % (self.netbios_domain, USER_NAME)
+ expected = calc_digest(name,
+ "",
+ self.password)
+ self._testWDigest(attribute, expected)
+
+ # Hash19 MD5(LOWER(NETBIOSDomainName\sAMAccountName),
+ # password)
+ #
+ def test_Wdigest19(self):
+ attribute = "virtualWDigest19"
+ name = "%s\\%s" % (self.netbios_domain, USER_NAME)
+ expected = calc_digest(name.lower(),
+ "",
+ self.password)
+ self._testWDigest(attribute, expected)
+
+ # Hash20 MD5(UPPER(NETBIOSDomainName\sAMAccountName),
+ # password)
+ #
+ def test_Wdigest20(self):
+ attribute = "virtualWDigest20"
+ name = "%s\\%s" % (self.netbios_domain, USER_NAME)
+ expected = calc_digest(name.upper(),
+ "",
+ self.password)
+ self._testWDigest(attribute, expected)
+
+ # Hash21 MD5(sAMAccountName,
+ # "Digest",
+ # password)
+ #
+ def test_Wdigest21(self):
+ attribute = "virtualWDigest21"
+ expected = calc_digest(USER_NAME,
+ "Digest",
+ self.password)
+ self._testWDigest(attribute, expected)
+
+ # Hash22 MD5(LOWER(sAMAccountName),
+ # "Digest",
+ # password)
+ #
+ def test_Wdigest22(self):
+ attribute = "virtualWDigest22"
+ expected = calc_digest(USER_NAME.lower(),
+ "Digest",
+ self.password)
+ self._testWDigest(attribute, expected)
+
+ # Hash23 MD5(UPPER(sAMAccountName),
+ # "Digest",
+ # password)
+ #
+ def test_Wdigest23(self):
+ attribute = "virtualWDigest23"
+ expected = calc_digest(USER_NAME.upper(),
+ "Digest",
+ self.password)
+ self._testWDigest(attribute, expected)
+
+ # Hash24 MD5(userPrincipalName),
+ # "Digest",
+ # password)
+ #
+ def test_Wdigest24(self):
+ attribute = "virtualWDigest24"
+ name = "%s@%s" % (USER_NAME, self.dns_domain)
+ expected = calc_digest(name,
+ "Digest",
+ self.password)
+ self._testWDigest(attribute, expected)
+
+ # Hash25 MD5(LOWER(userPrincipalName),
+ # "Digest",
+ # password)
+ #
+ def test_Wdigest25(self):
+ attribute = "virtualWDigest25"
+ name = "%s@%s" % (USER_NAME, self.dns_domain.lower())
+ expected = calc_digest(name.lower(),
+ "Digest",
+ self.password)
+ self._testWDigest(attribute, expected)
+
+ # Hash26 MD5(UPPER(userPrincipalName),
+ # "Digest",
+ # password)
+ #
+ def test_Wdigest26(self):
+ attribute = "virtualWDigest26"
+ name = "%s@%s" % (USER_NAME, self.dns_domain.lower())
+ expected = calc_digest(name.upper(),
+ "Digest",
+ self.password)
+ self._testWDigest(attribute, expected)
+ # Hash27 MD5(NETBIOSDomainName\sAMAccountName,
+ # "Digest",
+ # password)
+ #
+
+ def test_Wdigest27(self):
+ attribute = "virtualWDigest27"
+ name = "%s\\%s" % (self.netbios_domain, USER_NAME)
+ expected = calc_digest(name,
+ "Digest",
+ self.password)
+ self._testWDigest(attribute, expected)
+
+ # Hash28 MD5(LOWER(NETBIOSDomainName\sAMAccountName),
+ # "Digest",
+ # password)
+ #
+ def test_Wdigest28(self):
+ attribute = "virtualWDigest28"
+ name = "%s\\%s" % (self.netbios_domain.lower(), USER_NAME.lower())
+ expected = calc_digest(name,
+ "Digest",
+ self.password)
+ self._testWDigest(attribute, expected)
+
+ # Hash29 MD5(UPPER(NETBIOSDomainName\sAMAccountName),
+ # "Digest",
+ # password)
+ #
+ def test_Wdigest29(self):
+ attribute = "virtualWDigest29"
+ name = "%s\\%s" % (self.netbios_domain.upper(), USER_NAME.upper())
+ expected = calc_digest(name,
+ "Digest",
+ self.password)
+ self._testWDigest(attribute, expected)
+
+ def test_Wdigest30(self):
+ attribute = "virtualWDigest30"
+ self._testWDigest(attribute, None, True)
+
+ # Check digest calculation against an known htdigest value
+ def test_calc_digest(self):
+ htdigest = "gary:fred:2204fcc247cb47ded249ef2fe0013255"
+ digest = calc_digest("gary", "fred", "password")
+ self.assertEqual(htdigest, digest)
diff --git a/python/samba/tests/samba_tool/visualize.py b/python/samba/tests/samba_tool/visualize.py
new file mode 100644
index 0000000..f736129
--- /dev/null
+++ b/python/samba/tests/samba_tool/visualize.py
@@ -0,0 +1,618 @@
+# -*- coding: utf-8 -*-
+# Tests for samba-tool visualize
+# Copyright (C) Andrew Bartlett 2015, 2018
+#
+# by Douglas Bagnall <douglas.bagnall@catalyst.net.nz>
+#
+# 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/>.
+#
+"""Tests for samba-tool visualize ntdsconn using the test ldif
+topologies.
+
+We don't test samba-tool visualize reps here because repsTo and
+repsFrom are not replicated, and there are no actual remote servers to
+query.
+"""
+import os
+import tempfile
+import re
+from io import StringIO
+from samba.tests.samba_tool.base import SambaToolCmdTest
+from samba.kcc import ldif_import_export
+from samba.graph import COLOUR_SETS
+from samba.param import LoadParm
+
+MULTISITE_LDIF = os.path.join(os.environ['SRCDIR_ABS'],
+ "testdata/ldif-utils-test-multisite.ldif")
+
+# UNCONNECTED_LDIF is a single site, unconnected 5DC database that was
+# created using samba-tool domain join in testenv.
+UNCONNECTED_LDIF = os.path.join(os.environ['SRCDIR_ABS'],
+ "testdata/unconnected-intrasite.ldif")
+
+DOMAIN = "DC=ad,DC=samba,DC=example,DC=com"
+DN_TEMPLATE = "CN=%s,CN=Servers,CN=%s,CN=Sites,CN=Configuration," + DOMAIN
+
+MULTISITE_LDIF_DSAS = [
+ ("WIN01", "Default-First-Site-Name"),
+ ("WIN08", "Site-4"),
+ ("WIN07", "Site-4"),
+ ("WIN06", "Site-3"),
+ ("WIN09", "Site-5"),
+ ("WIN10", "Site-5"),
+ ("WIN02", "Site-2"),
+ ("WIN04", "Site-2"),
+ ("WIN03", "Site-2"),
+ ("WIN05", "Site-2"),
+]
+
+
+class StringIOThinksItIsATTY(StringIO):
+ """A StringIO that claims to be a TTY for testing --color=auto,
+ by switching the stringIO class attribute."""
+ def isatty(self):
+ return True
+
+
+def samdb_from_ldif(ldif, tempdir, lp, dsa=None, tag=''):
+ if dsa is None:
+ dsa_name = 'default-DSA'
+ else:
+ dsa_name = dsa[:5]
+ dburl = os.path.join(tempdir,
+ ("ldif-to-sambdb-%s-%s" %
+ (tag, dsa_name)))
+ samdb = ldif_import_export.ldif_to_samdb(dburl, lp, ldif,
+ forced_local_dsa=dsa)
+ return (samdb, dburl)
+
+
+def collapse_space(s, keep_empty_lines=False):
+ lines = []
+ for line in s.splitlines():
+ line = ' '.join(line.strip().split())
+ if line or keep_empty_lines:
+ lines.append(line)
+ return '\n'.join(lines)
+
+
+class SambaToolVisualizeLdif(SambaToolCmdTest):
+ def setUp(self):
+ super().setUp()
+ self.lp = LoadParm()
+ self.samdb, self.dbfile = samdb_from_ldif(MULTISITE_LDIF,
+ self.tempdir,
+ self.lp)
+ self.dburl = 'tdb://' + self.dbfile
+
+ def tearDown(self):
+ self.remove_files(self.dbfile)
+ super().tearDown()
+
+ def remove_files(self, *files):
+ for f in files:
+ self.assertTrue(f.startswith(self.tempdir))
+ os.unlink(f)
+
+ def test_colour(self):
+ """Ensure the colour output is the same as the monochrome output
+ EXCEPT for the colours, of which the monochrome one should
+ know nothing."""
+ colour_re = re.compile('\033' r'\[[\d;]+m')
+ result, monochrome, err = self.runsubcmd("visualize", "ntdsconn",
+ '-H', self.dburl,
+ '--color=no', '-S')
+ self.assertCmdSuccess(result, monochrome, err)
+ self.assertFalse(colour_re.findall(monochrome))
+
+ colour_args = [['--color=yes']]
+ colour_args += [['--color-scheme', x] for x in COLOUR_SETS
+ if x is not None]
+
+ for args in colour_args:
+ result, out, err = self.runsubcmd("visualize", "ntdsconn",
+ '-H', self.dburl,
+ '-S', *args)
+ self.assertCmdSuccess(result, out, err)
+ self.assertTrue(colour_re.search(out),
+ f"'{' '.join(args)}' should be colour")
+ uncoloured = colour_re.sub('', out)
+
+ self.assertStringsEqual(monochrome, uncoloured, strip=True)
+
+ def assert_colour(self, text, has_colour=True, monochrome=None):
+ colour_re = re.compile('\033' r'\[[\d;]+m')
+ found = colour_re.search(text)
+ if has_colour:
+ self.assertTrue(found, text)
+ else:
+ self.assertFalse(found, text)
+ if monochrome is not None:
+ uncoloured = colour_re.sub('', text)
+ self.assertStringsEqual(monochrome, uncoloured, strip=True)
+
+ def test_colour_auto_tty(self):
+ """Assert the behaviour of --colour=auto with and without
+ NO_COLOUR on a fake tty"""
+ result, monochrome, err = self.runsubcmd("visualize", "ntdsconn",
+ '-H', self.dburl,
+ '--color=no', '-S')
+ self.assertCmdSuccess(result, monochrome, err)
+ self.assert_colour(monochrome, False)
+ cls = self.__class__
+
+ try:
+ cls.stringIO = StringIOThinksItIsATTY
+ old_no_color = os.environ.pop('NO_COLOR', None)
+ # First with no NO_COLOR env var. There should be colour.
+ result, out, err = self.runsubcmd("visualize", "ntdsconn",
+ '-H', self.dburl,
+ '-S',
+ '--color=auto')
+ self.assertCmdSuccess(result, out, err)
+ self.assert_colour(out, True, monochrome)
+
+ for env, opt, is_colour in [
+ # NO_COLOR='' should be as if no NO_COLOR
+ ['', '--color=auto', True],
+ # NO_COLOR='1': we expect no colour
+ ['1', '--color=auto', False],
+ # NO_COLOR='no': we still expect no colour
+ ['no', '--color=auto', False],
+ # NO_COLOR=' ', alias for 'auto'
+ [' ', '--color=tty', False],
+ # NO_COLOR=' ', alias for 'auto'
+ [' ', '--color=if-tty', False],
+ # NO_COLOR='', alias for 'auto'
+ ['', '--color=tty', True],
+ # NO_COLOR='', alias for 'no'
+ ['', '--color=never', False],
+ # NO_COLOR='x', alias for 'yes' (--color=yes wins)
+ ['x', '--color=force', True],
+ ]:
+ os.environ['NO_COLOR'] = env
+
+ try:
+ result, out, err = self.runsubcmd("visualize", "ntdsconn",
+ '-H', self.dburl,
+ '-S',
+ opt)
+ except SystemExit:
+ # optparse makes us do this
+ self.fail(f"optparse rejects {env}, {opt}, {is_colour}")
+
+ self.assertCmdSuccess(result, out, err)
+ self.assert_colour(out, is_colour, monochrome)
+
+ # with "-o -" output filename alias for stdout.
+ result, out, err = self.runsubcmd("visualize", "ntdsconn",
+ '-H', self.dburl,
+ '-S',
+ opt,
+ '-o', '-')
+ self.assertCmdSuccess(result, out, err)
+ self.assert_colour(out, is_colour, monochrome)
+
+ finally:
+ cls.stringIO = StringIO
+ if old_no_color is None:
+ os.environ.pop('NO_COLOR', None)
+ else:
+ os.environ['NO_COLOR'] = old_no_color
+
+ def test_import_ldif_xdot(self):
+ """We can't test actual xdot, but using the environment we can
+ persuade samba-tool that a script we write is xdot and ensure
+ it gets the right text.
+ """
+ result, expected, err = self.runsubcmd("visualize", "ntdsconn",
+ '-H', self.dburl,
+ '--color=no', '-S',
+ '--dot')
+ self.assertCmdSuccess(result, expected, err)
+
+ # not that we're expecting anything here
+ old_xdot_path = os.environ.get('SAMBA_TOOL_XDOT_PATH')
+
+ tmpdir = tempfile.mkdtemp()
+ fake_xdot = os.path.join(tmpdir, 'fake_xdot')
+ content = os.path.join(tmpdir, 'content')
+ f = open(fake_xdot, 'w')
+ print('#!/bin/sh', file=f)
+ print('cp $1 %s' % content, file=f)
+ f.close()
+ os.chmod(fake_xdot, 0o700)
+
+ os.environ['SAMBA_TOOL_XDOT_PATH'] = fake_xdot
+ result, empty, err = self.runsubcmd("visualize", "ntdsconn",
+ '--importldif', MULTISITE_LDIF,
+ '--color=no', '-S',
+ '--xdot')
+
+ f = open(content)
+ xdot = f.read()
+ f.close()
+ os.remove(fake_xdot)
+ os.remove(content)
+ os.rmdir(tmpdir)
+
+ if old_xdot_path is not None:
+ os.environ['SAMBA_TOOL_XDOT_PATH'] = old_xdot_path
+ else:
+ del os.environ['SAMBA_TOOL_XDOT_PATH']
+
+ self.assertCmdSuccess(result, xdot, err)
+ self.assertStringsEqual(expected, xdot, strip=True)
+
+ def test_import_ldif(self):
+ """Make sure the samba-tool visualize --importldif option gives the
+ same output as using the externally generated db from the same
+ LDIF."""
+ result, s1, err = self.runsubcmd("visualize", "ntdsconn",
+ '-H', self.dburl,
+ '--color=no', '-S')
+ self.assertCmdSuccess(result, s1, err)
+
+ result, s2, err = self.runsubcmd("visualize", "ntdsconn",
+ '--importldif', MULTISITE_LDIF,
+ '--color=no', '-S')
+ self.assertCmdSuccess(result, s2, err)
+
+ self.assertStringsEqual(s1, s2)
+
+ def test_output_file(self):
+ """Check that writing to a file works, with and without
+ --color=auto."""
+ # NOTE, we can't really test --color=auto works with a TTY.
+ colour_re = re.compile('\033' r'\[[\d;]+m')
+ result, expected, err = self.runsubcmd("visualize", "ntdsconn",
+ '-H', self.dburl,
+ '--color=auto', '-S')
+ self.assertCmdSuccess(result, expected, err)
+ # Not a TTY, so stdout output should be colourless
+ self.assertFalse(colour_re.search(expected))
+ expected = expected.strip()
+
+ color_auto_file = os.path.join(self.tempdir, 'color-auto')
+
+ result, out, err = self.runsubcmd("visualize", "ntdsconn",
+ '-H', self.dburl,
+ '--color=auto', '-S',
+ '-o', color_auto_file)
+ self.assertCmdSuccess(result, out, err)
+ # We wrote to file, so stdout should be empty
+ self.assertEqual(out, '')
+ f = open(color_auto_file)
+ color_auto = f.read()
+ f.close()
+ self.assertStringsEqual(color_auto, expected, strip=True)
+ self.remove_files(color_auto_file)
+
+ color_no_file = os.path.join(self.tempdir, 'color-no')
+ result, out, err = self.runsubcmd("visualize", "ntdsconn",
+ '-H', self.dburl,
+ '--color=no', '-S',
+ '-o', color_no_file)
+ self.assertCmdSuccess(result, out, err)
+ self.assertEqual(out, '')
+ f = open(color_no_file)
+ color_no = f.read()
+ f.close()
+ self.remove_files(color_no_file)
+
+ self.assertStringsEqual(color_no, expected, strip=True)
+
+ color_yes_file = os.path.join(self.tempdir, 'color-yes')
+ result, out, err = self.runsubcmd("visualize", "ntdsconn",
+ '-H', self.dburl,
+ '--color=yes', '-S',
+ '-o', color_yes_file)
+ self.assertCmdSuccess(result, out, err)
+ self.assertEqual(out, '')
+ f = open(color_yes_file)
+ colour_yes = f.read()
+ f.close()
+ self.assertNotEqual(colour_yes.strip(), expected)
+
+ self.remove_files(color_yes_file)
+
+ # Try the magic filename "-", meaning stdout.
+ # This doesn't exercise the case when stdout is a TTY
+ for c, equal in [('no', True), ('auto', True), ('yes', False)]:
+ result, out, err = self.runsubcmd("visualize", "ntdsconn",
+ '-H', self.dburl,
+ '--color', c,
+ '-S', '-o', '-')
+ self.assertCmdSuccess(result, out, err)
+ self.assertEqual((out.strip() == expected), equal)
+
+ def test_utf8(self):
+ """Ensure that --utf8 adds at least some expected utf-8, and that it
+ isn't there without --utf8."""
+ result, utf8, err = self.runsubcmd("visualize", "ntdsconn",
+ '-H', self.dburl,
+ '--color=no', '-S', '--utf8')
+ self.assertCmdSuccess(result, utf8, err)
+
+ result, ascii, err = self.runsubcmd("visualize", "ntdsconn",
+ '-H', self.dburl,
+ '--color=no', '-S')
+ self.assertCmdSuccess(result, ascii, err)
+ for c in ('│', '─', '╭'):
+ self.assertTrue(c in utf8, 'UTF8 should contain %s' % c)
+ self.assertTrue(c not in ascii, 'ASCII should not contain %s' % c)
+
+ def test_forced_local_dsa(self):
+ # the forced_local_dsa shouldn't make any difference, except
+ # for the title line.
+ result, target, err = self.runsubcmd("visualize", "ntdsconn",
+ '-H', self.dburl,
+ '--color=no', '-S')
+ self.assertCmdSuccess(result, target, err)
+ files = []
+ target = target.strip().split('\n', 1)[1]
+ for cn, site in MULTISITE_LDIF_DSAS:
+ dsa = DN_TEMPLATE % (cn, site)
+ samdb, dbfile = samdb_from_ldif(MULTISITE_LDIF,
+ self.tempdir,
+ self.lp, dsa,
+ tag=cn)
+
+ result, out, err = self.runsubcmd("visualize", "ntdsconn",
+ '-H', 'tdb://' + dbfile,
+ '--color=no', '-S')
+ self.assertCmdSuccess(result, out, err)
+ # Separate out the title line, which will differ in the DN.
+ title, body = out.strip().split('\n', 1)
+ self.assertStringsEqual(target, body)
+ self.assertIn(cn, title)
+ files.append(dbfile)
+ self.remove_files(*files)
+
+ def test_short_names(self):
+ """Ensure the colour ones are the same as the monochrome ones EXCEPT
+ for the colours, of which the monochrome one should know nothing"""
+ result, short, err = self.runsubcmd("visualize", "ntdsconn",
+ '-H', self.dburl,
+ '--color=no', '-S', '--no-key')
+ self.assertCmdSuccess(result, short, err)
+ result, long, err = self.runsubcmd("visualize", "ntdsconn",
+ '-H', self.dburl,
+ '--color=no', '--no-key')
+ self.assertCmdSuccess(result, long, err)
+
+ lines = short.split('\n')
+ replacements = []
+ key_lines = ['']
+ short_without_key = []
+ for line in lines:
+ m = re.match(r"'(.{1,2})' stands for '(.+)'", line)
+ if m:
+ a, b = m.groups()
+ replacements.append((len(a), a, b))
+ key_lines.append(line)
+ else:
+ short_without_key.append(line)
+
+ short = '\n'.join(short_without_key)
+ # we need to replace longest strings first
+ replacements.sort(reverse=True)
+ short2long = short
+ # we don't want to shorten the DC name in the header line.
+ long_header, long2short = long.strip().split('\n', 1)
+ for _, a, b in replacements:
+ short2long = short2long.replace(a, b)
+ long2short = long2short.replace(b, a)
+
+ long2short = '%s\n%s' % (long_header, long2short)
+
+ # The white space is going to be all wacky, so lets squish it down
+ short2long = collapse_space(short2long)
+ long2short = collapse_space(long2short)
+ short = collapse_space(short)
+ long = collapse_space(long)
+
+ self.assertStringsEqual(short2long, long, strip=True)
+ self.assertStringsEqual(short, long2short, strip=True)
+
+ def test_disconnected_ldif_with_key(self):
+ """Test that the 'unconnected' ldif shows up and exactly matches the
+ expected output."""
+ # This is not truly a disconnected graph because the
+ # vampre/local/promoted DCs are in there and they have
+ # relationships, and SERVER2 and SERVER3 for some reason refer
+ # to them.
+
+ samdb, dbfile = samdb_from_ldif(UNCONNECTED_LDIF,
+ self.tempdir,
+ self.lp, tag='disconnected')
+ dburl = 'tdb://' + dbfile
+ result, output, err = self.runsubcmd("visualize", "ntdsconn",
+ '-H', dburl,
+ '--color=no', '-S')
+ self.remove_files(dbfile)
+ self.assertCmdSuccess(result, output, err)
+ self.assertStringsEqual(output,
+ EXPECTED_DISTANCE_GRAPH_WITH_KEY)
+
+ def test_dot_ntdsconn(self):
+ """Graphviz NTDS Connection output"""
+ result, dot, err = self.runsubcmd("visualize", "ntdsconn",
+ '-H', self.dburl,
+ '--color=no', '-S', '--dot',
+ '--no-key')
+ self.assertCmdSuccess(result, dot, err)
+ self.assertStringsEqual(EXPECTED_DOT_MULTISITE_NO_KEY, dot)
+
+ def test_dot_ntdsconn_disconnected(self):
+ """Graphviz NTDS Connection output from disconnected graph"""
+ samdb, dbfile = samdb_from_ldif(UNCONNECTED_LDIF,
+ self.tempdir,
+ self.lp, tag='disconnected')
+
+ result, dot, err = self.runsubcmd("visualize", "ntdsconn",
+ '-H', 'tdb://' + dbfile,
+ '--color=no', '-S', '--dot',
+ '-o', '-')
+ self.assertCmdSuccess(result, dot, err)
+ self.remove_files(dbfile)
+ self.assertStringsEqual(EXPECTED_DOT_NTDSCONN_DISCONNECTED, dot,
+ strip=True)
+
+ def test_dot_ntdsconn_disconnected_to_file(self):
+ """Graphviz NTDS Connection output into a file"""
+ samdb, dbfile = samdb_from_ldif(UNCONNECTED_LDIF,
+ self.tempdir,
+ self.lp, tag='disconnected')
+
+ dot_file = os.path.join(self.tempdir, 'dotfile')
+
+ result, dot, err = self.runsubcmd("visualize", "ntdsconn",
+ '-H', 'tdb://' + dbfile,
+ '--color=no', '-S', '--dot',
+ '-o', dot_file)
+ self.assertCmdSuccess(result, dot, err)
+ f = open(dot_file)
+ dot = f.read()
+ f.close()
+ self.assertStringsEqual(EXPECTED_DOT_NTDSCONN_DISCONNECTED, dot)
+
+ self.remove_files(dbfile, dot_file)
+
+
+EXPECTED_DOT_MULTISITE_NO_KEY = r"""/* generated by samba */
+digraph A_samba_tool_production {
+label="NTDS Connections known to CN=WIN01,CN=Servers,CN=Default-First-Site-Name,CN=Sites,CN=Configuration,DC=ad,DC=samba,DC=example,DC=com";
+fontsize=10;
+
+node[fontname=Helvetica; fontsize=10];
+
+"CN=NTDS Settings,\nCN=WIN01,\nCN=Servers,\nCN=Default-\nFirst-Site-Name,\n...";
+"CN=NTDS Settings,\nCN=WIN02,\nCN=Servers,\nCN=Site-2,\n...";
+"CN=NTDS Settings,\nCN=WIN03,\nCN=Servers,\nCN=Site-2,\n...";
+"CN=NTDS Settings,\nCN=WIN04,\nCN=Servers,\nCN=Site-2,\n...";
+"CN=NTDS Settings,\nCN=WIN05,\nCN=Servers,\nCN=Site-2,\n...";
+"CN=NTDS Settings,\nCN=WIN06,\nCN=Servers,\nCN=Site-3,\n...";
+"CN=NTDS Settings,\nCN=WIN07,\nCN=Servers,\nCN=Site-4,\n...";
+"CN=NTDS Settings,\nCN=WIN08,\nCN=Servers,\nCN=Site-4,\n...";
+"CN=NTDS Settings,\nCN=WIN09,\nCN=Servers,\nCN=Site-5,\n...";
+"CN=NTDS Settings,\nCN=WIN10,\nCN=Servers,\nCN=Site-5,\n...";
+"CN=NTDS Settings,\nCN=WIN01,\nCN=Servers,\nCN=Default-\nFirst-Site-Name,\n..." -> "CN=NTDS Settings,\nCN=WIN03,\nCN=Servers,\nCN=Site-2,\n..." [color="#000000", ];
+"CN=NTDS Settings,\nCN=WIN01,\nCN=Servers,\nCN=Default-\nFirst-Site-Name,\n..." -> "CN=NTDS Settings,\nCN=WIN06,\nCN=Servers,\nCN=Site-3,\n..." [color="#000000", ];
+"CN=NTDS Settings,\nCN=WIN01,\nCN=Servers,\nCN=Default-\nFirst-Site-Name,\n..." -> "CN=NTDS Settings,\nCN=WIN07,\nCN=Servers,\nCN=Site-4,\n..." [color="#000000", ];
+"CN=NTDS Settings,\nCN=WIN01,\nCN=Servers,\nCN=Default-\nFirst-Site-Name,\n..." -> "CN=NTDS Settings,\nCN=WIN08,\nCN=Servers,\nCN=Site-4,\n..." [color="#000000", ];
+"CN=NTDS Settings,\nCN=WIN01,\nCN=Servers,\nCN=Default-\nFirst-Site-Name,\n..." -> "CN=NTDS Settings,\nCN=WIN10,\nCN=Servers,\nCN=Site-5,\n..." [color="#000000", ];
+"CN=NTDS Settings,\nCN=WIN02,\nCN=Servers,\nCN=Site-2,\n..." -> "CN=NTDS Settings,\nCN=WIN04,\nCN=Servers,\nCN=Site-2,\n..." [color="#000000", ];
+"CN=NTDS Settings,\nCN=WIN02,\nCN=Servers,\nCN=Site-2,\n..." -> "CN=NTDS Settings,\nCN=WIN05,\nCN=Servers,\nCN=Site-2,\n..." [color="#000000", ];
+"CN=NTDS Settings,\nCN=WIN03,\nCN=Servers,\nCN=Site-2,\n..." -> "CN=NTDS Settings,\nCN=WIN04,\nCN=Servers,\nCN=Site-2,\n..." [color="#000000", ];
+"CN=NTDS Settings,\nCN=WIN03,\nCN=Servers,\nCN=Site-2,\n..." -> "CN=NTDS Settings,\nCN=WIN05,\nCN=Servers,\nCN=Site-2,\n..." [color="#000000", ];
+"CN=NTDS Settings,\nCN=WIN04,\nCN=Servers,\nCN=Site-2,\n..." -> "CN=NTDS Settings,\nCN=WIN01,\nCN=Servers,\nCN=Default-\nFirst-Site-Name,\n..." [color="#000000", ];
+"CN=NTDS Settings,\nCN=WIN04,\nCN=Servers,\nCN=Site-2,\n..." -> "CN=NTDS Settings,\nCN=WIN02,\nCN=Servers,\nCN=Site-2,\n..." [color="#000000", ];
+"CN=NTDS Settings,\nCN=WIN04,\nCN=Servers,\nCN=Site-2,\n..." -> "CN=NTDS Settings,\nCN=WIN03,\nCN=Servers,\nCN=Site-2,\n..." [color="#000000", ];
+"CN=NTDS Settings,\nCN=WIN05,\nCN=Servers,\nCN=Site-2,\n..." -> "CN=NTDS Settings,\nCN=WIN02,\nCN=Servers,\nCN=Site-2,\n..." [color="#000000", ];
+"CN=NTDS Settings,\nCN=WIN05,\nCN=Servers,\nCN=Site-2,\n..." -> "CN=NTDS Settings,\nCN=WIN03,\nCN=Servers,\nCN=Site-2,\n..." [color="#000000", ];
+"CN=NTDS Settings,\nCN=WIN07,\nCN=Servers,\nCN=Site-4,\n..." -> "CN=NTDS Settings,\nCN=WIN01,\nCN=Servers,\nCN=Default-\nFirst-Site-Name,\n..." [color="#000000", ];
+"CN=NTDS Settings,\nCN=WIN09,\nCN=Servers,\nCN=Site-5,\n..." -> "CN=NTDS Settings,\nCN=WIN10,\nCN=Servers,\nCN=Site-5,\n..." [color="#000000", ];
+"CN=NTDS Settings,\nCN=WIN10,\nCN=Servers,\nCN=Site-5,\n..." -> "CN=NTDS Settings,\nCN=WIN01,\nCN=Servers,\nCN=Default-\nFirst-Site-Name,\n..." [color="#000000", ];
+"CN=NTDS Settings,\nCN=WIN10,\nCN=Servers,\nCN=Site-5,\n..." -> "CN=NTDS Settings,\nCN=WIN09,\nCN=Servers,\nCN=Site-5,\n..." [color="#000000", ];
+}
+
+"""
+
+
+EXPECTED_DOT_NTDSCONN_DISCONNECTED = r"""/* generated by samba */
+digraph A_samba_tool_production {
+label="NTDS Connections known to CN=LOCALDC,CN=Servers,CN=Default-First-Site-Name,CN=Sites,CN=Configuration,DC=samba,DC=example,DC=com";
+fontsize=10;
+
+node[fontname=Helvetica; fontsize=10];
+
+"CN=NTDS Settings,\nCN=CLIENT,\n...";
+"CN=NTDS Settings,\nCN=LOCALDC,\n...";
+"CN=NTDS Settings,\nCN=PROMOTEDVDC,\n...";
+"CN=NTDS Settings,\nCN=SERVER1,\n...";
+"CN=NTDS Settings,\nCN=SERVER2,\n...";
+"CN=NTDS Settings,\nCN=SERVER3,\n...";
+"CN=NTDS Settings,\nCN=SERVER4,\n...";
+"CN=NTDS Settings,\nCN=SERVER5,\n...";
+"CN=NTDS Settings,\nCN=LOCALDC,\n..." -> "CN=NTDS Settings,\nCN=PROMOTEDVDC,\n..." [color="#000000", ];
+"CN=NTDS Settings,\nCN=PROMOTEDVDC,\n..." -> "CN=NTDS Settings,\nCN=LOCALDC,\n..." [color="#000000", ];
+"CN=NTDS Settings,\nCN=SERVER2,\n..." -> "CN=NTDS Settings,\nCN=PROMOTEDVDC,\n..." [color="#000000", ];
+"CN=NTDS Settings,\nCN=SERVER3,\n..." -> "CN=NTDS Settings,\nCN=LOCALDC,\n..." [color="#000000", ];
+subgraph cluster_key {
+label="Key";
+subgraph cluster_key_nodes {
+label="";
+color = "invis";
+
+}
+subgraph cluster_key_edges {
+label="";
+color = "invis";
+subgraph cluster_key_0_ {
+key_0_e1[label=src; color="#000000"; group="key_0__g"]
+key_0_e2[label=dest; color="#000000"; group="key_0__g"]
+key_0_e1 -> key_0_e2 [constraint = false; color="#000000"]
+key_0__label[shape=plaintext; style=solid; width=2.000000; label="NTDS Connection\r"]
+}
+{key_0__label}
+}
+
+elision0[shape=plaintext; style=solid; label="\“...” means “CN=Servers,CN=Default-First-Site-Name,CN=Sites,CN=Configuration,DC=samba,DC=example,DC=com”\r"]
+
+}
+"CN=NTDS Settings,\nCN=CLIENT,\n..." -> key_0__label [style=invis];
+"CN=NTDS Settings,\nCN=LOCALDC,\n..." -> key_0__label [style=invis];
+"CN=NTDS Settings,\nCN=PROMOTEDVDC,\n..." -> key_0__label [style=invis];
+"CN=NTDS Settings,\nCN=SERVER1,\n..." -> key_0__label [style=invis];
+"CN=NTDS Settings,\nCN=SERVER2,\n..." -> key_0__label [style=invis];
+"CN=NTDS Settings,\nCN=SERVER3,\n..." -> key_0__label [style=invis];
+"CN=NTDS Settings,\nCN=SERVER4,\n..." -> key_0__label [style=invis];
+"CN=NTDS Settings,\nCN=SERVER5,\n..." -> key_0__label [style=invis]
+key_0__label -> elision0 [style=invis; weight=9]
+
+}
+"""
+
+EXPECTED_DISTANCE_GRAPH_WITH_KEY = """
+NTDS Connections known to CN=LOCALDC,CN=Servers,CN=Default-First-Site-Name,CN=Sites,CN=Configuration,DC=samba,DC=example,DC=com
+
+ destination
+ ,-------- *,CN=CLIENT+
+ |,------- *,CN=LOCALDC+
+ ||,------ *,CN=PROMOTEDVDC+
+ |||,----- *,CN=SERVER1+
+ ||||,---- *,CN=SERVER2+
+ |||||,--- *,CN=SERVER3+
+ ||||||,-- *,CN=SERVER4+
+ source |||||||,- *,CN=SERVER5+
+ *,CN=CLIENT+ 0-------
+ *,CN=LOCALDC+ -01-----
+*,CN=PROMOTEDVDC+ -10-----
+ *,CN=SERVER1+ ---0----
+ *,CN=SERVER2+ -21-0---
+ *,CN=SERVER3+ -12--0--
+ *,CN=SERVER4+ ------0-
+ *,CN=SERVER5+ -------0
+
+'*' stands for 'CN=NTDS Settings'
+'+' stands for ',CN=Servers,CN=Default-First-Site-Name,CN=Sites,CN=Configuration,DC=samba,DC=example,DC=com'
+
+Data can get from source to destination in the indicated number of steps.
+0 means zero steps (it is the same DC)
+1 means a direct link
+2 means a transitive link involving two steps (i.e. one intermediate DC)
+- means there is no connection, even through other DCs
+
+"""
diff --git a/python/samba/tests/samba_tool/visualize_drs.py b/python/samba/tests/samba_tool/visualize_drs.py
new file mode 100644
index 0000000..64b2cdb
--- /dev/null
+++ b/python/samba/tests/samba_tool/visualize_drs.py
@@ -0,0 +1,636 @@
+# -*- coding: utf-8 -*-
+# Originally based on tests for samba.kcc.ldif_import_export.
+# Copyright (C) Andrew Bartlett 2015, 2018
+#
+# by Douglas Bagnall <douglas.bagnall@catalyst.net.nz>
+#
+# 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/>.
+#
+"""Tests for samba-tool visualize using the vampire DC and promoted DC
+environments. For most tests we assume we can't assert much about what
+state they are in, so we mainly check for command failure, but for
+others we try to grasp control of replication and make more specific
+assertions.
+"""
+
+import os
+import re
+import json
+import random
+import subprocess
+from samba.tests.samba_tool.base import SambaToolCmdTest
+
+VERBOSE = False
+
+ENV_DSAS = {
+ 'promoted_dc': ['CN=PROMOTEDVDC,CN=Servers,CN=Default-First-Site-Name,CN=Sites,CN=Configuration,DC=samba,DC=example,DC=com',
+ 'CN=LOCALDC,CN=Servers,CN=Default-First-Site-Name,CN=Sites,CN=Configuration,DC=samba,DC=example,DC=com'],
+ 'vampire_dc': ['CN=LOCALDC,CN=Servers,CN=Default-First-Site-Name,CN=Sites,CN=Configuration,DC=samba,DC=example,DC=com',
+ 'CN=LOCALVAMPIREDC,CN=Servers,CN=Default-First-Site-Name,CN=Sites,CN=Configuration,DC=samba,DC=example,DC=com'],
+}
+
+PARTITION_NAMES = [
+ "DOMAIN",
+ "CONFIGURATION",
+ "SCHEMA",
+ "DNSDOMAIN",
+ "DNSFOREST",
+]
+
+def adjust_cmd_for_py_version(parts):
+ if os.getenv("PYTHON", None):
+ parts.insert(0, os.environ["PYTHON"])
+ return parts
+
+def set_auto_replication(dc, allow):
+ credstring = '-U%s%%%s' % (os.environ["USERNAME"], os.environ["PASSWORD"])
+ on_or_off = '-' if allow else '+'
+
+ for opt in ['DISABLE_INBOUND_REPL',
+ 'DISABLE_OUTBOUND_REPL']:
+ cmd = adjust_cmd_for_py_version(['bin/samba-tool',
+ 'drs', 'options',
+ credstring, dc,
+ "--dsa-option=%s%s" % (on_or_off, opt)])
+
+ subprocess.check_call(cmd)
+
+
+def force_replication(src, dest, base):
+ credstring = '-U%s%%%s' % (os.environ["USERNAME"], os.environ["PASSWORD"])
+ cmd = adjust_cmd_for_py_version(['bin/samba-tool',
+ 'drs', 'replicate',
+ dest, src, base,
+ credstring,
+ '--sync-forced'])
+
+ subprocess.check_call(cmd)
+
+
+def get_utf8_matrix(s):
+ # parse the graphical table *just* well enough for our tests
+ # decolourise first
+ s = re.sub("\033" r"\[[^m]+m", '', s)
+ lines = s.split('\n')
+ # matrix rows have '·' on the diagonal
+ rows = [x.strip().replace('·', '0') for x in lines if '·' in x]
+ names = []
+ values = []
+ for r in rows:
+ parts = r.rsplit(None, len(rows))
+ k, v = parts[0], parts[1:]
+ # we want the FOO in 'CN=FOO+' or 'CN=FOO,CN=x,DC=...'
+ k = re.match(r'cn=([^+,]+)', k.lower()).group(1)
+ names.append(k)
+ if len(v) == 1: # this is a single-digit matrix, no spaces
+ v = list(v[0])
+ values.append([int(x) if x.isdigit() else 1e999 for x in v])
+
+ d = {}
+ for n1, row in zip(names, values):
+ d[n1] = {}
+ for n2, v in zip(names, row):
+ d[n1][n2] = v
+
+ return d
+
+
+class SambaToolVisualizeDrsTest(SambaToolCmdTest):
+
+ def test_ntdsconn(self):
+ server = "ldap://%s" % os.environ["SERVER"]
+ creds = "%s%%%s" % (os.environ["USERNAME"], os.environ["PASSWORD"])
+ (result, out, err) = self.runsubcmd("visualize", "ntdsconn",
+ '-H', server,
+ '-U', creds,
+ '--color=no', '-S')
+ self.assertCmdSuccess(result, out, err)
+
+ def test_ntdsconn_remote(self):
+ server = "ldap://%s" % os.environ["SERVER"]
+ creds = "%s%%%s" % (os.environ["USERNAME"], os.environ["PASSWORD"])
+ (result, out, err) = self.runsubcmd("visualize", "ntdsconn",
+ '-H', server,
+ '-U', creds,
+ '--color=no', '-S', '-r')
+ self.assertCmdSuccess(result, out, err)
+
+ def test_reps(self):
+ server = "ldap://%s" % os.environ["SERVER"]
+ creds = "%s%%%s" % (os.environ["USERNAME"], os.environ["PASSWORD"])
+ (result, out, err) = self.runsubcmd("visualize", "reps",
+ '-H', server,
+ '-U', creds,
+ '--color=no', '-S')
+ self.assertCmdSuccess(result, out, err)
+
+ def test_uptodateness_all_partitions(self):
+ creds = "%s%%%s" % (os.environ["USERNAME"], os.environ["PASSWORD"])
+ dc1 = os.environ["SERVER"]
+ dc2 = os.environ["DC_SERVER"]
+ # We will check that the visualisation works for the two
+ # stopped DCs, but we can't make assertions that the output
+ # will be the same because there may be replication between
+ # the two calls. Stopping the replication on these ones is not
+ # enough because there are other DCs about.
+ (result, out, err) = self.runsubcmd("visualize", "uptodateness",
+ "-r",
+ '-H', "ldap://%s" % dc1,
+ '-U', creds,
+ '--color=no', '-S')
+ self.assertCmdSuccess(result, out, err)
+
+ (result, out, err) = self.runsubcmd("visualize", "uptodateness",
+ "-r",
+ '-H', "ldap://%s" % dc2,
+ '-U', creds,
+ '--color=no', '-S')
+ self.assertCmdSuccess(result, out, err)
+
+ def test_uptodateness_partitions(self):
+ creds = "%s%%%s" % (os.environ["USERNAME"], os.environ["PASSWORD"])
+ dc1 = os.environ["SERVER"]
+ for part in PARTITION_NAMES:
+ (result, out, err) = self.runsubcmd("visualize", "uptodateness",
+ "-r",
+ '-H', "ldap://%s" % dc1,
+ '-U', creds,
+ '--color=no', '-S',
+ '--partition', part)
+ self.assertCmdSuccess(result, out, err)
+
+ def test_drs_uptodateness(self):
+ """
+ Test cmd `drs uptodateness`
+
+ It should print info like this:
+
+ DNSDOMAIN failure: 4 median: 1.5 maximum: 2
+ SCHEMA failure: 4 median: 220.0 maximum: 439
+ DOMAIN failure: 1 median: 25 maximum: 25
+ CONFIGURATION failure: 1 median: 25 maximum: 25
+ DNSFOREST failure: 4 median: 1.5 maximum: 2
+
+ """
+ creds = "%s%%%s" % (os.environ["USERNAME"], os.environ["PASSWORD"])
+ dc1 = os.environ["SERVER"]
+ dc2 = os.environ["DC_SERVER"]
+ for dc in [dc1, dc2]:
+ (result, out, err) = self.runsubcmd("drs", "uptodateness",
+ '-H', "ldap://%s" % dc,
+ '-U', creds)
+ self.assertCmdSuccess(result, out, err)
+ # each partition name should be in output
+ for part_name in PARTITION_NAMES:
+ self.assertIn(part_name, out, msg=out)
+
+ for line in out.splitlines():
+ # check keyword in output
+ for attr in ['maximum', 'median', 'failure']:
+ self.assertIn(attr, line)
+
+ def test_drs_uptodateness_partition(self):
+ """
+ Test cmd `drs uptodateness --partition DOMAIN`
+
+ It should print info like this:
+
+ DOMAIN failure: 1 median: 25 maximum: 25
+
+ """
+ creds = "%s%%%s" % (os.environ["USERNAME"], os.environ["PASSWORD"])
+ dc1 = os.environ["SERVER"]
+ dc2 = os.environ["DC_SERVER"]
+ for dc in [dc1, dc2]:
+ (result, out, err) = self.runsubcmd("drs", "uptodateness",
+ '-H', "ldap://%s" % dc,
+ '-U', creds,
+ '--partition', 'DOMAIN')
+ self.assertCmdSuccess(result, out, err)
+
+ lines = out.splitlines()
+ self.assertEqual(len(lines), 1)
+
+ line = lines[0]
+ self.assertTrue(line.startswith('DOMAIN'))
+
+ def test_drs_uptodateness_json(self):
+ """
+ Test cmd `drs uptodateness --json`
+
+ Example output:
+
+ {
+ "DNSDOMAIN": {
+ "failure": 0,
+ "median": 0.0,
+ "maximum": 0
+ },
+ ...
+ "SCHEMA": {
+ "failure": 0,
+ "median": 0.0,
+ "maximum": 0
+ }
+ }
+ """
+ creds = "%s%%%s" % (os.environ["USERNAME"], os.environ["PASSWORD"])
+ dc1 = os.environ["SERVER"]
+ dc2 = os.environ["DC_SERVER"]
+ for dc in [dc1, dc2]:
+ (result, out, err) = self.runsubcmd("drs", "uptodateness",
+ '-H', "ldap://%s" % dc,
+ '-U', creds,
+ '--json')
+ self.assertCmdSuccess(result, out, err)
+ # should be json format
+ obj = json.loads(out)
+ # each partition name should be in json obj
+ for part_name in PARTITION_NAMES:
+ self.assertIn(part_name, obj)
+ summary_obj = obj[part_name]
+ for attr in ['maximum', 'median', 'failure']:
+ self.assertIn(attr, summary_obj)
+
+ def test_drs_uptodateness_json_median(self):
+ """
+ Test cmd `drs uptodateness --json --median`
+
+ drs uptodateness --json --median
+
+ {
+ "DNSDOMAIN": {
+ "median": 0.0
+ },
+ ...
+ "SCHEMA": {
+ "median": 0.0
+ }
+ }
+ """
+ creds = "%s%%%s" % (os.environ["USERNAME"], os.environ["PASSWORD"])
+ dc1 = os.environ["SERVER"]
+ dc2 = os.environ["DC_SERVER"]
+ for dc in [dc1, dc2]:
+ (result, out, err) = self.runsubcmd("drs", "uptodateness",
+ '-H', "ldap://%s" % dc,
+ '-U', creds,
+ '--json', '--median')
+ self.assertCmdSuccess(result, out, err)
+ # should be json format
+ obj = json.loads(out)
+ # each partition name should be in json obj
+ for part_name in PARTITION_NAMES:
+ self.assertIn(part_name, obj)
+ summary_obj = obj[part_name]
+ self.assertIn('median', summary_obj)
+ self.assertNotIn('maximum', summary_obj)
+ self.assertNotIn('failure', summary_obj)
+
+ def assert_matrix_validity(self, matrix, dcs=()):
+ for dc in dcs:
+ self.assertIn(dc, matrix)
+ for k, row in matrix.items():
+ self.assertEqual(row[k], 0)
+
+ def test_uptodateness_stop_replication_domain(self):
+ creds = "%s%%%s" % (os.environ["USERNAME"], os.environ["PASSWORD"])
+ dc1 = os.environ["SERVER"]
+ dc2 = os.environ["DC_SERVER"]
+ self.addCleanup(set_auto_replication, dc1, True)
+ self.addCleanup(set_auto_replication, dc2, True)
+
+ def display(heading, out):
+ if VERBOSE:
+ print("========", heading, "=========")
+ print(out)
+
+ samdb1 = self.getSamDB("-H", "ldap://%s" % dc1, "-U", creds)
+ samdb2 = self.getSamDB("-H", "ldap://%s" % dc2, "-U", creds)
+
+ domain_dn = samdb1.domain_dn()
+ self.assertTrue(domain_dn == samdb2.domain_dn(),
+ "We expected the same domain_dn across DCs")
+
+ ou1 = "OU=dc1.%x,%s" % (random.randrange(1 << 64), domain_dn)
+ ou2 = "OU=dc2.%x,%s" % (random.randrange(1 << 64), domain_dn)
+ samdb1.add({
+ "dn": ou1,
+ "objectclass": "organizationalUnit"
+ })
+ samdb2.add({
+ "dn": ou2,
+ "objectclass": "organizationalUnit"
+ })
+
+ set_auto_replication(dc1, False)
+ (result, out, err) = self.runsubcmd("visualize", "uptodateness",
+ "-r",
+ '-H', "ldap://%s" % dc1,
+ '-U', creds,
+ '--color=yes',
+ '--utf8', '-S',
+ '--partition', 'DOMAIN')
+ display("dc1 replication is now off", out)
+ self.assertCmdSuccess(result, out, err)
+ matrix = get_utf8_matrix(out)
+ self.assert_matrix_validity(matrix, [dc1, dc2])
+
+ force_replication(dc2, dc1, domain_dn)
+ (result, out, err) = self.runsubcmd("visualize", "uptodateness",
+ "-r",
+ '-H', "ldap://%s" % dc1,
+ '-U', creds,
+ '--color=yes',
+ '--utf8', '-S',
+ '--partition', 'DOMAIN')
+ display("forced replication %s -> %s" % (dc2, dc1), out)
+ self.assertCmdSuccess(result, out, err)
+ matrix = get_utf8_matrix(out)
+ self.assert_matrix_validity(matrix, [dc1, dc2])
+ self.assertEqual(matrix[dc1][dc2], 0)
+
+ force_replication(dc1, dc2, domain_dn)
+ (result, out, err) = self.runsubcmd("visualize", "uptodateness",
+ "-r",
+ '-H', "ldap://%s" % dc1,
+ '-U', creds,
+ '--color=yes',
+ '--utf8', '-S',
+ '--partition', 'DOMAIN')
+ display("forced replication %s -> %s" % (dc2, dc1), out)
+ self.assertCmdSuccess(result, out, err)
+ matrix = get_utf8_matrix(out)
+ self.assert_matrix_validity(matrix, [dc1, dc2])
+ self.assertEqual(matrix[dc2][dc1], 0)
+
+ dn1 = 'cn=u1.%%d,%s' % (ou1)
+ dn2 = 'cn=u2.%%d,%s' % (ou2)
+
+ for i in range(10):
+ samdb1.add({
+ "dn": dn1 % i,
+ "objectclass": "user"
+ })
+
+ (result, out, err) = self.runsubcmd("visualize", "uptodateness",
+ "-r",
+ '-H', "ldap://%s" % dc1,
+ '-U', creds,
+ '--color=yes',
+ '--utf8', '-S',
+ '--partition', 'DOMAIN')
+ display("added 10 users on %s" % dc1, out)
+ self.assertCmdSuccess(result, out, err)
+ matrix = get_utf8_matrix(out)
+ self.assert_matrix_validity(matrix, [dc1, dc2])
+ # dc2's view of dc1 should now be 10 changes out of date
+ self.assertEqual(matrix[dc2][dc1], 10)
+
+ for i in range(10):
+ samdb2.add({
+ "dn": dn2 % i,
+ "objectclass": "user"
+ })
+
+ (result, out, err) = self.runsubcmd("visualize", "uptodateness",
+ "-r",
+ '-H', "ldap://%s" % dc1,
+ '-U', creds,
+ '--color=yes',
+ '--utf8', '-S',
+ '--partition', 'DOMAIN')
+ display("added 10 users on %s" % dc2, out)
+ self.assertCmdSuccess(result, out, err)
+ matrix = get_utf8_matrix(out)
+ self.assert_matrix_validity(matrix, [dc1, dc2])
+ # dc1's view of dc2 is probably 11 changes out of date
+ self.assertGreaterEqual(matrix[dc1][dc2], 10)
+
+ for i in range(10, 101):
+ samdb1.add({
+ "dn": dn1 % i,
+ "objectclass": "user"
+ })
+ samdb2.add({
+ "dn": dn2 % i,
+ "objectclass": "user"
+ })
+
+ (result, out, err) = self.runsubcmd("visualize", "uptodateness",
+ "-r",
+ '-H', "ldap://%s" % dc1,
+ '-U', creds,
+ '--color=yes',
+ '--utf8', '-S',
+ '--partition', 'DOMAIN')
+ display("added 91 users on both", out)
+ self.assertCmdSuccess(result, out, err)
+ matrix = get_utf8_matrix(out)
+ self.assert_matrix_validity(matrix, [dc1, dc2])
+ # the difference here should be ~101.
+ self.assertGreaterEqual(matrix[dc1][dc2], 100)
+ self.assertGreaterEqual(matrix[dc2][dc1], 100)
+
+ (result, out, err) = self.runsubcmd("visualize", "uptodateness",
+ "-r",
+ '-H', "ldap://%s" % dc1,
+ '-U', creds,
+ '--color=yes',
+ '--utf8', '-S',
+ '--partition', 'DOMAIN',
+ '--max-digits', '2')
+ display("with --max-digits 2", out)
+ self.assertCmdSuccess(result, out, err)
+ matrix = get_utf8_matrix(out)
+ self.assert_matrix_validity(matrix, [dc1, dc2])
+ # visualising with 2 digits mean these overflow into infinity
+ self.assertGreaterEqual(matrix[dc1][dc2], 1e99)
+ self.assertGreaterEqual(matrix[dc2][dc1], 1e99)
+
+ (result, out, err) = self.runsubcmd("visualize", "uptodateness",
+ "-r",
+ '-H', "ldap://%s" % dc1,
+ '-U', creds,
+ '--color=yes',
+ '--utf8', '-S',
+ '--partition', 'DOMAIN',
+ '--max-digits', '1')
+ display("with --max-digits 1", out)
+ self.assertCmdSuccess(result, out, err)
+ matrix = get_utf8_matrix(out)
+ self.assert_matrix_validity(matrix, [dc1, dc2])
+ # visualising with 1 digit means these overflow into infinity
+ self.assertGreaterEqual(matrix[dc1][dc2], 1e99)
+ self.assertGreaterEqual(matrix[dc2][dc1], 1e99)
+
+ force_replication(dc2, dc1, samdb1.domain_dn())
+ (result, out, err) = self.runsubcmd("visualize", "uptodateness",
+ "-r",
+ '-H', "ldap://%s" % dc1,
+ '-U', creds,
+ '--color=yes',
+ '--utf8', '-S',
+ '--partition', 'DOMAIN')
+
+ display("forced replication %s -> %s" % (dc2, dc1), out)
+ self.assertCmdSuccess(result, out, err)
+ matrix = get_utf8_matrix(out)
+ self.assert_matrix_validity(matrix, [dc1, dc2])
+ self.assertEqual(matrix[dc1][dc2], 0)
+
+ force_replication(dc1, dc2, samdb2.domain_dn())
+ (result, out, err) = self.runsubcmd("visualize", "uptodateness",
+ "-r",
+ '-H', "ldap://%s" % dc1,
+ '-U', creds,
+ '--color=yes',
+ '--utf8', '-S',
+ '--partition', 'DOMAIN')
+
+ display("forced replication %s -> %s" % (dc1, dc2), out)
+ self.assertCmdSuccess(result, out, err)
+ matrix = get_utf8_matrix(out)
+ self.assert_matrix_validity(matrix, [dc1, dc2])
+ self.assertEqual(matrix[dc2][dc1], 0)
+
+ samdb1.delete(ou1, ['tree_delete:1'])
+ samdb2.delete(ou2, ['tree_delete:1'])
+
+ (result, out, err) = self.runsubcmd("visualize", "uptodateness",
+ "-r",
+ '-H', "ldap://%s" % dc1,
+ '-U', creds,
+ '--color=yes',
+ '--utf8', '-S',
+ '--partition', 'DOMAIN')
+ display("tree delete both ous on %s" % (dc1,), out)
+ self.assertCmdSuccess(result, out, err)
+ matrix = get_utf8_matrix(out)
+ self.assert_matrix_validity(matrix, [dc1, dc2])
+ self.assertGreaterEqual(matrix[dc1][dc2], 100)
+ self.assertGreaterEqual(matrix[dc2][dc1], 100)
+
+ set_auto_replication(dc1, True)
+ (result, out, err) = self.runsubcmd("visualize", "uptodateness",
+ "-r",
+ '-H', "ldap://%s" % dc1,
+ '-U', creds,
+ '--color=yes',
+ '--utf8', '-S',
+ '--partition', 'DOMAIN')
+ display("replication is now on", out)
+ self.assertCmdSuccess(result, out, err)
+ matrix = get_utf8_matrix(out)
+ self.assert_matrix_validity(matrix, [dc1, dc2])
+ # We can't assert actual values after this because
+ # auto-replication is on and things will change underneath us.
+
+ (result, out, err) = self.runsubcmd("visualize", "uptodateness",
+ "-r",
+ '-H', "ldap://%s" % dc2,
+ '-U', creds,
+ '--color=yes',
+ '--utf8', '-S',
+ '--partition', 'DOMAIN')
+
+ display("%s's view" % dc2, out)
+ self.assertCmdSuccess(result, out, err)
+ matrix = get_utf8_matrix(out)
+ self.assert_matrix_validity(matrix, [dc1, dc2])
+
+ force_replication(dc1, dc2, samdb2.domain_dn())
+ (result, out, err) = self.runsubcmd("visualize", "uptodateness",
+ "-r",
+ '-H', "ldap://%s" % dc1,
+ '-U', creds,
+ '--color=yes',
+ '--utf8', '-S',
+ '--partition', 'DOMAIN')
+
+ display("forced replication %s -> %s" % (dc1, dc2), out)
+ self.assertCmdSuccess(result, out, err)
+ matrix = get_utf8_matrix(out)
+ self.assert_matrix_validity(matrix, [dc1, dc2])
+
+ force_replication(dc2, dc1, samdb2.domain_dn())
+ (result, out, err) = self.runsubcmd("visualize", "uptodateness",
+ "-r",
+ '-H', "ldap://%s" % dc1,
+ '-U', creds,
+ '--color=yes',
+ '--utf8', '-S',
+ '--partition', 'DOMAIN')
+ display("forced replication %s -> %s" % (dc2, dc1), out)
+ self.assertCmdSuccess(result, out, err)
+ matrix = get_utf8_matrix(out)
+ self.assert_matrix_validity(matrix, [dc1, dc2])
+
+ (result, out, err) = self.runsubcmd("visualize", "uptodateness",
+ "-r",
+ '-H', "ldap://%s" % dc2,
+ '-U', creds,
+ '--color=yes',
+ '--utf8', '-S',
+ '--partition', 'DOMAIN')
+ display("%s's view" % dc2, out)
+
+ self.assertCmdSuccess(result, out, err)
+ matrix = get_utf8_matrix(out)
+ self.assert_matrix_validity(matrix, [dc1, dc2])
+
+ def test_reps_remote(self):
+ server = "ldap://%s" % os.environ["SERVER"]
+ creds = "%s%%%s" % (os.environ["USERNAME"], os.environ["PASSWORD"])
+ (result, out, err) = self.runsubcmd("visualize", "reps",
+ '-H', server,
+ '-U', creds,
+ '--color=no', '-S', '-r')
+ self.assertCmdSuccess(result, out, err)
+
+ def test_ntdsconn_dot(self):
+ server = "ldap://%s" % os.environ["SERVER"]
+ creds = "%s%%%s" % (os.environ["USERNAME"], os.environ["PASSWORD"])
+ (result, out, err) = self.runsubcmd("visualize", "ntdsconn",
+ '-H', server,
+ '-U', creds, '--dot',
+ '--color=no', '-S')
+ self.assertCmdSuccess(result, out, err)
+
+ def test_ntdsconn_remote_dot(self):
+ server = "ldap://%s" % os.environ["SERVER"]
+ creds = "%s%%%s" % (os.environ["USERNAME"], os.environ["PASSWORD"])
+ (result, out, err) = self.runsubcmd("visualize", "ntdsconn",
+ '-H', server,
+ '-U', creds, '--dot',
+ '--color=no', '-S', '-r')
+ self.assertCmdSuccess(result, out, err)
+
+ def test_reps_dot(self):
+ server = "ldap://%s" % os.environ["SERVER"]
+ creds = "%s%%%s" % (os.environ["USERNAME"], os.environ["PASSWORD"])
+ (result, out, err) = self.runsubcmd("visualize", "reps",
+ '-H', server,
+ '-U', creds, '--dot',
+ '--color=no', '-S')
+ self.assertCmdSuccess(result, out, err)
+
+ def test_reps_remote_dot(self):
+ server = "ldap://%s" % os.environ["SERVER"]
+ creds = "%s%%%s" % (os.environ["USERNAME"], os.environ["PASSWORD"])
+ (result, out, err) = self.runsubcmd("visualize", "reps",
+ '-H', server,
+ '-U', creds, '--dot',
+ '--color=no', '-S', '-r')
+ self.assertCmdSuccess(result, out, err)